defmodule Kagi.Client do @moduledoc """ Reusable configuration for Kagi requests. A client stores the configured session token and request options. Building a client performs no network I/O. ## Fields * `:session_token` - Kagi session token string. * `:req_options` - keyword list merged into each `Req` request. """ alias Kagi.Error @typedoc "A configured Kagi client." @type t :: %__MODULE__{ session_token: String.t(), req_options: keyword() } defstruct [:session_token, req_options: []] @doc """ Builds a `%Kagi.Client{}` from application config. Reads `:session_token` and `:req_options` from `:kagi_ex` config. Returns `{:error, %Kagi.Error{}}` when the token is missing or `:req_options` is not a keyword list. ## Examples config :kagi_ex, session_token: "abc" {:ok, client} = Kagi.Client.new() {:error, %Kagi.Error{reason: :missing_session_token}} = Kagi.Client.new() """ @spec new() :: {:ok, t()} | {:error, Error.t()} def new do with {:ok, session_token} <- Kagi.SessionToken.resolve(), {:ok, req_options} <- validate_keyword_option(:req_options) do {:ok, %__MODULE__{session_token: session_token, req_options: req_options}} end end @doc """ Builds a `%Kagi.Client{}` or raises `Kagi.Error`. """ @spec new!() :: t() def new! do case new() do {:ok, client} -> client {:error, %Error{} = error} -> raise error end end @spec configured(atom(), term()) :: term() defp configured(key, default) do Application.get_env(:kagi_ex, key, default) end @spec validate_keyword_option(atom()) :: {:ok, keyword()} | {:error, Error.t()} defp validate_keyword_option(key) do value = configured(key, []) if Keyword.keyword?(value) do {:ok, value} else {:error, Error.new(:invalid_option, "#{key} must be a keyword list")} end end end