defmodule Kagi do @moduledoc """ Typed client for [Kagi](https://kagi.com) Search, Summarizer, and Maps. Configure `:session_token` before calling the one-off helpers, or build a reusable `Kagi.Client` with `new/0`. Requests are built with `Req` and sent through `CloakedReq`. ## Quick start config :kagi_ex, session_token: "..." client = Kagi.new!() {:ok, search} = Kagi.search(client, "elixir req", lens: :programming, limit: 5) {:ok, summary} = Kagi.summarize(client, "https://elixir-lang.org") {:ok, places} = Kagi.maps(client, "coffee zurich", ll: "47.3769,8.5417") Non-bang functions return `{:ok, struct}` or `{:error, %Kagi.Error{}}`. Bang functions return the struct or raise `Kagi.Error`. """ alias Kagi.Client alias Kagi.Error alias Kagi.Maps alias Kagi.Search alias Kagi.Summary @typedoc """ Query argument accepted by `search/1..3` and `maps/1..3`. Lists are joined with spaces before the request is made. """ @type query :: String.t() | [String.t()] @doc """ Builds a reusable `Kagi.Client`. Reads `:session_token` and `:req_options` from application config. If the session token is missing or invalid, returns `{:error, %Kagi.Error{reason: :missing_session_token}}`. ## Application config * `:session_token` - Kagi session token string. * `:req_options` - keyword list merged into every `Req` request. Returns `{:error, %Kagi.Error{reason: :invalid_option}}` when `:req_options` is not a keyword list. ## Examples config :kagi_ex, session_token: "abc" {:ok, client} = Kagi.new() """ @spec new() :: {:ok, Client.t()} | {:error, Error.t()} defdelegate new, to: Client @doc """ Builds a reusable `Kagi.Client` or raises `Kagi.Error`. """ @spec new!() :: Client.t() defdelegate new!, to: Client @doc """ Searches Kagi and returns typed results. Accepts a prebuilt `Kagi.Client`. Use `search/2` or `search/1` to build the client from application config. ## Search options * `:limit` - maximum result count (default `10`); applied client-side. * `:region` - region code such as `"ch"`, `"us"`, `"de"`, or `"no_region"`. * `:lens` - `:default`, `:programming`, `:forums`, `:pdfs`, `:non_commercial`, or `:world_news`. * `:sort` - `:recency`, `:website`, or `:ad_trackers`. * `:time` - `:day`, `:week`, `:month`, or `:year`. * `:from` - start date as `YYYY-MM-DD`; cannot be combined with `:time`. * `:to` - end date as `YYYY-MM-DD`; cannot be combined with `:time`. * `:site` - appends a `site:` filter to the query. * `:filetype` - appends a `filetype:` filter to the query. * `:verbatim` - disables query expansion when `true`. Returns `{:error, %Kagi.Error{}}` for invalid options, HTTP failures, CAPTCHA/challenge pages, and parse failures. ## Examples client = Kagi.new!() {:ok, %Kagi.Search{results: results}} = Kagi.search(client, "elixir req http client", lens: :programming, limit: 3) Kagi.search("elixir lang", limit: 5) """ @spec search(Client.t(), query(), keyword()) :: {:ok, Search.t()} | {:error, Error.t()} def search(%Client{} = client, query, options) do Search.request(client, query, options) end @doc """ Searches Kagi with either a prebuilt client or application config. `search(client, query)` uses default search options. `search(query, options)` builds a client from application config and applies the supplied search options. """ @spec search(Client.t(), query()) :: {:ok, Search.t()} | {:error, Error.t()} @spec search(query(), keyword()) :: {:ok, Search.t()} | {:error, Error.t()} def search(%Client{} = client, query), do: search(client, query, []) def search(query, options) when is_list(options) do with {:ok, client} <- Client.new() do Search.request(client, query, options) end end @doc """ Searches Kagi using application config and default search options. Requires `config :kagi_ex, :session_token, "..."` or returns `{:error, %Kagi.Error{reason: :missing_session_token}}`. """ @spec search(query()) :: {:ok, Search.t()} | {:error, Error.t()} def search(query), do: search(query, []) @doc """ Searches Kagi and raises `Kagi.Error` on failure. """ @spec search!(Client.t(), query(), keyword()) :: Search.t() def search!(%Client{} = client, query, options) do unwrap!(search(client, query, options)) end @doc """ Searches Kagi and raises `Kagi.Error` on failure. """ @spec search!(Client.t(), query()) :: Search.t() @spec search!(query(), keyword()) :: Search.t() def search!(%Client{} = client, query), do: search!(client, query, []) def search!(query, options) when is_list(options) do unwrap!(search(query, options)) end @doc """ Searches Kagi with application config and raises `Kagi.Error` on failure. """ @spec search!(query()) :: Search.t() def search!(query), do: search!(query, []) @doc """ Summarizes a single URL with Kagi Summarizer. Accepts a prebuilt `Kagi.Client`. Use `summarize/2` or `summarize/1` to build the client from application config. ## Summary options * `:type` - `:summary` (default) or `:takeaway`. * `:lang` - target language code, default `"EN"`. Returns `{:error, %Kagi.Error{}}` for invalid options, HTTP failures, and parse failures. ## Examples client = Kagi.new!() {:ok, %Kagi.Summary{summary: markdown}} = Kagi.summarize(client, "https://www.rust-lang.org/learn", type: :takeaway) """ @spec summarize(Client.t(), String.t(), keyword()) :: {:ok, Summary.t()} | {:error, Error.t()} def summarize(%Client{} = client, url, options) do Summary.request(client, url, options) end @doc """ Summarizes a URL with either a prebuilt client or application config. `summarize(client, url)` uses default summary options. `summarize(url, options)` builds a client from application config and applies the supplied summary options. """ @spec summarize(Client.t(), String.t()) :: {:ok, Summary.t()} | {:error, Error.t()} @spec summarize(String.t(), keyword()) :: {:ok, Summary.t()} | {:error, Error.t()} def summarize(%Client{} = client, url), do: summarize(client, url, []) def summarize(url, options) when is_list(options) do with {:ok, client} <- Client.new() do Summary.request(client, url, options) end end @doc """ Summarizes a URL using application config and default summary options. """ @spec summarize(String.t()) :: {:ok, Summary.t()} | {:error, Error.t()} def summarize(url), do: summarize(url, []) @doc """ Summarizes a URL and raises `Kagi.Error` on failure. """ @spec summarize!(Client.t(), String.t(), keyword()) :: Summary.t() def summarize!(%Client{} = client, url, options) do unwrap!(summarize(client, url, options)) end @doc """ Summarizes a URL and raises `Kagi.Error` on failure. """ @spec summarize!(Client.t(), String.t()) :: Summary.t() @spec summarize!(String.t(), keyword()) :: Summary.t() def summarize!(%Client{} = client, url), do: summarize!(client, url, []) def summarize!(url, options) when is_list(options) do unwrap!(summarize(url, options)) end @doc """ Summarizes a URL with application config and raises `Kagi.Error` on failure. """ @spec summarize!(String.t()) :: Summary.t() def summarize!(url), do: summarize!(url, []) @doc """ Searches Kagi Maps for places matching a query. Accepts a prebuilt `Kagi.Client`. Use `maps/2` or `maps/1` to build the client from application config. ## Maps options * `:limit` - maximum result count (default `10`); applied client-side after sorting. * `:ll` - center coordinate as `"LAT,LON"` (e.g. `"47.3769,8.5417"`). * `:bbox` - bounding box as `"WEST,SOUTH,EAST,NORTH"`. * `:zoom` - zoom level as a number. * `:sort` - `:relevance` (server order), `:rating`, `:distance`, or `:price`. Price sorts by `$`-string length. * `:order` - `:asc` or `:desc`. Defaults are `:desc` for `:rating`, `:asc` for `:distance` and `:price`. `nil` values always sort last. Sorting and the limit apply client-side, after the API response is parsed. ## Examples client = Kagi.new!() {:ok, %Kagi.Maps{results: results}} = Kagi.maps(client, "coffee zurich", ll: "47.3769,8.5417", sort: :rating) """ @spec maps(Client.t(), query(), keyword()) :: {:ok, Maps.t()} | {:error, Error.t()} def maps(%Client{} = client, query, options) do Maps.request(client, query, options) end @doc """ Searches Kagi Maps with either a prebuilt client or application config. `maps(client, query)` uses default Maps options. `maps(query, options)` builds a client from application config and applies the supplied Maps options. """ @spec maps(Client.t(), query()) :: {:ok, Maps.t()} | {:error, Error.t()} @spec maps(query(), keyword()) :: {:ok, Maps.t()} | {:error, Error.t()} def maps(%Client{} = client, query), do: maps(client, query, []) def maps(query, options) when is_list(options) do with {:ok, client} <- Client.new() do Maps.request(client, query, options) end end @doc """ Searches Kagi Maps using application config and default Maps options. """ @spec maps(query()) :: {:ok, Maps.t()} | {:error, Error.t()} def maps(query), do: maps(query, []) @doc """ Searches Kagi Maps and raises `Kagi.Error` on failure. """ @spec maps!(Client.t(), query(), keyword()) :: Maps.t() def maps!(%Client{} = client, query, options) do unwrap!(maps(client, query, options)) end @doc """ Searches Kagi Maps and raises `Kagi.Error` on failure. """ @spec maps!(Client.t(), query()) :: Maps.t() @spec maps!(query(), keyword()) :: Maps.t() def maps!(%Client{} = client, query), do: maps!(client, query, []) def maps!(query, options) when is_list(options) do unwrap!(maps(query, options)) end @doc """ Searches Kagi Maps with application config and raises `Kagi.Error` on failure. """ @spec maps!(query()) :: Maps.t() def maps!(query), do: maps!(query, []) @spec unwrap!({:ok, value} | {:error, Error.t()}) :: value when value: var defp unwrap!({:ok, value}), do: value defp unwrap!({:error, %Error{} = error}), do: raise(error) end