참고소스 수정본

This commit is contained in:
LASTA_DEV01\lasta
2026-05-12 19:40:31 +09:00
parent 0f34a451fc
commit 2e9204243d
8708 changed files with 3259488 additions and 869 deletions

View File

@@ -0,0 +1,3 @@
[
inputs: ["{mix,.formatter}.exs", "generate.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

View File

@@ -0,0 +1,25 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Temporary files, for example, from tests.
/tmp/
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
firecrawl-*.tar
firecrawl-*/

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Sideguide Technologies Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,104 @@
# Firecrawl
Auto-generated Elixir client for the [Firecrawl API v2](https://docs.firecrawl.dev/api-reference).
Built with [`Req`](https://hexdocs.pm/req) — minimal, idiomatic, auto-generated from the OpenAPI spec with [`NimbleOptions`](https://hexdocs.pm/nimble_options) validation.
## Installation
Add `firecrawl` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:firecrawl, "~> 1.0"}
]
end
```
## Configuration
Set your API key in application config:
```elixir
config :firecrawl, api_key: "fc-your-api-key"
```
Or pass it as an option to any function:
```elixir
Firecrawl.scrape_and_extract_from_url([url: "https://example.com"], api_key: "fc-your-api-key")
```
If no API key is found in config or options, a `RuntimeError` is raised with instructions.
## Usage
All params are passed as keyword lists with snake_case keys. Invalid keys, missing required params, and type errors are caught immediately by `NimbleOptions`.
```elixir
# Scrape a URL
{:ok, response} = Firecrawl.scrape_and_extract_from_url(
url: "https://example.com",
formats: ["markdown"]
)
# Crawl a site
{:ok, response} = Firecrawl.crawl_urls(
url: "https://example.com",
limit: 100,
sitemap: :skip
)
# Map URLs
{:ok, response} = Firecrawl.map_urls(url: "https://example.com")
# Search
{:ok, response} = Firecrawl.search_and_scrape(query: "firecrawl web scraping")
# Check crawl status
{:ok, response} = Firecrawl.get_crawl_status("job-uuid")
# Parse a file (PDF, DOCX, HTML, etc.)
{:ok, response} = Firecrawl.parse_file(
[filename: "report.pdf", data: File.read!("report.pdf"), content_type: "application/pdf"],
formats: ["markdown"]
)
# Self-hosted instance
{:ok, response} = Firecrawl.scrape_and_extract_from_url(
[url: "https://example.com"],
base_url: "https://your-instance.com/v2"
)
```
### Bang variants
Every function has a `!` variant that raises on error instead of returning `{:error, _}`:
```elixir
response = Firecrawl.scrape_and_extract_from_url!(url: "https://example.com")
```
## Regenerating from the OpenAPI Spec
The entire client is auto-generated from the Firecrawl OpenAPI specification. To regenerate after spec changes:
```bash
mix run generate.exs
```
This will:
1. Fetch the latest OpenAPI JSON from GitHub
2. Generate all API wrapper functions in `lib/firecrawl.ex`
3. Bump the version in `mix.exs` using semver (only if the generated code changed):
- **Major** bump if public functions were removed (breaking change)
- **Minor** bump if new public functions were added
- **Patch** bump for any other changes (signatures, docs, etc.)
Re-running when nothing changed is a no-op — the version is not bumped.
## License
MIT

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
defmodule Firecrawl.Error do
@moduledoc """
Exception raised when the Firecrawl API returns an error response (HTTP 4xx/5xx).
## Fields
* `:status` - The HTTP status code
* `:body` - The decoded response body (typically a map with `"error"` key)
"""
defexception [:status, :body]
@type t :: %__MODULE__{
status: pos_integer(),
body: term()
}
@impl true
def message(%__MODULE__{status: status, body: body}) when is_map(body) do
error_msg =
case body["error"] || body["message"] do
msg when is_binary(msg) -> msg
_ -> inspect(body)
end
"Firecrawl API error (HTTP #{status}): #{error_msg}"
end
def message(%__MODULE__{status: status, body: body}) do
"Firecrawl API error (HTTP #{status}): #{inspect(body)}"
end
end

View File

@@ -0,0 +1,48 @@
defmodule Firecrawl.MixProject do
use Mix.Project
@version "1.3.1"
@source_url "https://github.com/firecrawl/firecrawl/tree/main/apps/elixir-sdk"
def project do
[
app: :firecrawl,
version: @version,
elixir: "~> 1.15",
start_permanent: Mix.env() == :prod,
deps: deps(),
package: package(),
name: "Firecrawl",
description: "Auto-generated Elixir client for the Firecrawl API v2",
source_url: @source_url,
docs: [
main: "Firecrawl",
extras: ["README.md"]
]
]
end
def application do
[
extra_applications: [:logger]
]
end
defp deps do
[
{:req, "~> 0.5"},
{:nimble_options, "~> 1.1"},
{:ex_doc, "~> 0.34", only: :dev, runtime: false}
]
end
defp package do
[
files: ~w(lib .formatter.exs mix.exs README.md LICENSE),
licenses: ["MIT"],
links: %{
"GitHub" => @source_url
}
]
end
end

View File

@@ -0,0 +1,17 @@
%{
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"},
"finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"},
}

View File

@@ -0,0 +1,249 @@
defmodule FirecrawlTest do
use ExUnit.Case
test "raises when no API key is configured" do
old = Application.get_env(:firecrawl, :api_key)
Application.delete_env(:firecrawl, :api_key)
on_exit(fn -> if old, do: Application.put_env(:firecrawl, :api_key, old) end)
assert_raise RuntimeError, ~r/Firecrawl API key not found/, fn ->
Firecrawl.get_credit_usage()
end
end
test "does not raise when API key is in application config" do
Application.put_env(:firecrawl, :api_key, "test-config-key")
on_exit(fn -> Application.delete_env(:firecrawl, :api_key) end)
result = Firecrawl.get_queue_status(base_url: "http://localhost:1", retry: false)
assert {:error, _} = result
end
test "does not raise when API key is passed as option" do
old = Application.get_env(:firecrawl, :api_key)
Application.delete_env(:firecrawl, :api_key)
on_exit(fn -> if old, do: Application.put_env(:firecrawl, :api_key, old) end)
result =
Firecrawl.get_queue_status(
api_key: "test-opt-key",
base_url: "http://localhost:1",
retry: false
)
assert {:error, _} = result
end
test "non-bang returns {:error, _} for missing required params" do
Application.put_env(:firecrawl, :api_key, "test-key")
on_exit(fn -> Application.delete_env(:firecrawl, :api_key) end)
assert {:error, %NimbleOptions.ValidationError{}} =
Firecrawl.scrape_and_extract_from_url([])
end
test "non-bang returns {:error, _} for unknown keys (typo detection)" do
Application.put_env(:firecrawl, :api_key, "test-key")
on_exit(fn -> Application.delete_env(:firecrawl, :api_key) end)
assert {:error, %NimbleOptions.ValidationError{message: msg}} =
Firecrawl.scrape_and_extract_from_url(url: "https://example.com", typo_option: true)
assert msg =~ "unknown options"
end
test "non-bang returns {:error, _} for wrong types" do
Application.put_env(:firecrawl, :api_key, "test-key")
on_exit(fn -> Application.delete_env(:firecrawl, :api_key) end)
assert {:error, %NimbleOptions.ValidationError{}} =
Firecrawl.crawl_urls(url: "https://example.com", limit: "not an integer")
end
test "bang raises for missing required params" do
Application.put_env(:firecrawl, :api_key, "test-key")
on_exit(fn -> Application.delete_env(:firecrawl, :api_key) end)
assert_raise NimbleOptions.ValidationError, ~r/required/, fn ->
Firecrawl.scrape_and_extract_from_url!([])
end
end
test "bang raises for unknown keys" do
Application.put_env(:firecrawl, :api_key, "test-key")
on_exit(fn -> Application.delete_env(:firecrawl, :api_key) end)
assert_raise NimbleOptions.ValidationError, ~r/unknown options/, fn ->
Firecrawl.scrape_and_extract_from_url!(url: "https://example.com", typo_option: true)
end
end
test "accepts atom values for enum params" do
Application.put_env(:firecrawl, :api_key, "test-key")
on_exit(fn -> Application.delete_env(:firecrawl, :api_key) end)
result =
Firecrawl.crawl_urls(
[url: "https://example.com", sitemap: :skip],
base_url: "http://localhost:1",
retry: false
)
assert {:error, _} = result
end
test "accepts string values for enum params (model)" do
Application.put_env(:firecrawl, :api_key, "test-key")
on_exit(fn -> Application.delete_env(:firecrawl, :api_key) end)
result =
Firecrawl.start_agent(
[prompt: "test", model: "spark-1-mini"],
base_url: "http://localhost:1",
retry: false
)
assert {:error, err} = result
refute match?(%NimbleOptions.ValidationError{}, err),
"Expected connection error, got validation error: #{inspect(err)}"
end
test "accepts string values for enum params (sitemap)" do
Application.put_env(:firecrawl, :api_key, "test-key")
on_exit(fn -> Application.delete_env(:firecrawl, :api_key) end)
result =
Firecrawl.crawl_urls(
[url: "https://example.com", sitemap: "skip"],
base_url: "http://localhost:1",
retry: false
)
assert {:error, err} = result
refute match?(%NimbleOptions.ValidationError{}, err),
"Expected connection error, got validation error: #{inspect(err)}"
end
test "parse_file returns error tuple when filename is empty" do
Application.put_env(:firecrawl, :api_key, "test-key")
on_exit(fn -> Application.delete_env(:firecrawl, :api_key) end)
assert {:error, %ArgumentError{message: msg}} =
Firecrawl.parse_file([filename: "", data: "x"])
assert msg =~ "filename cannot be empty"
end
test "parse_file returns error tuple when data is nil" do
Application.put_env(:firecrawl, :api_key, "test-key")
on_exit(fn -> Application.delete_env(:firecrawl, :api_key) end)
assert {:error, %ArgumentError{message: msg}} =
Firecrawl.parse_file([filename: "doc.pdf", data: nil])
assert msg =~ "file data cannot be empty"
end
test "parse_file rejects unknown options" do
Application.put_env(:firecrawl, :api_key, "test-key")
on_exit(fn -> Application.delete_env(:firecrawl, :api_key) end)
assert {:error, %NimbleOptions.ValidationError{message: msg}} =
Firecrawl.parse_file(
[filename: "doc.pdf", data: "x"],
typo_option: true
)
assert msg =~ "unknown options"
end
test "non-bang returns {:error, %Firecrawl.Error{}} for API errors" do
adapter = fn request ->
resp = Req.Response.new(
status: 402,
headers: %{"content-type" => ["application/json"]},
body: Jason.encode!(%{"success" => false, "error" => "Payment required"})
)
{request, resp}
end
result =
Firecrawl.scrape_and_extract_from_url(
[url: "https://example.com"],
api_key: "test-key",
adapter: adapter
)
assert {:error, %Firecrawl.Error{status: 402}} = result
end
test "bang raises Firecrawl.Error for API errors" do
adapter = fn request ->
resp = Req.Response.new(
status: 401,
headers: %{"content-type" => ["application/json"]},
body: Jason.encode!(%{"success" => false, "error" => "Unauthorized"})
)
{request, resp}
end
assert_raise Firecrawl.Error, ~r/Unauthorized/, fn ->
Firecrawl.scrape_and_extract_from_url!(
[url: "https://example.com"],
api_key: "test-key",
adapter: adapter
)
end
end
test "non-bang returns {:ok, response} for successful API calls" do
adapter = fn request ->
resp = Req.Response.new(
status: 200,
headers: %{"content-type" => ["application/json"]},
body: Jason.encode!(%{"success" => true, "data" => %{}})
)
{request, resp}
end
result =
Firecrawl.get_credit_usage(
api_key: "test-key",
adapter: adapter
)
assert {:ok, %Req.Response{status: 200}} = result
end
test "all expected API functions are defined with bang variants" do
functions = Firecrawl.__info__(:functions)
expected = [
{:scrape_and_extract_from_url, 0},
{:scrape_and_extract_from_url, 1},
{:scrape_and_extract_from_url, 2},
{:scrape_and_extract_from_url!, 0},
{:scrape_and_extract_from_url!, 1},
{:scrape_and_extract_from_url!, 2},
{:crawl_urls, 0},
{:crawl_urls!, 0},
{:get_credit_usage, 0},
{:get_credit_usage!, 0},
{:get_queue_status, 0},
{:get_queue_status!, 0},
{:cancel_crawl, 1},
{:cancel_crawl!, 1},
{:parse_file, 1},
{:parse_file, 2},
{:parse_file, 3},
{:parse_file!, 1},
{:parse_file!, 2},
{:parse_file!, 3}
]
for {name, arity} <- expected do
assert {name, arity} in functions,
"Expected #{name}/#{arity} to be defined in Firecrawl"
end
end
end

View File

@@ -0,0 +1 @@
ExUnit.start()