defmodule CouchDB do @moduledoc """ Lightweight CouchDB client builder focused on authentication flows. The module exposes helpers to build a pre-configured `Tesla.Client` alongside a thin wrapper around `Tesla.request/2` to keep the original API surface available. """ alias CouchDB.Environment alias Tesla.Middleware @type interface :: :base | :backend | :admin @default_headers [{"user-agent", "couchdb_auth"}] @default_adapter {Tesla.Adapter.Hackney, []} @doc """ Build a `Tesla.Client` configured for the requested interface. The `interface` can be `:base` (default), `:backend`, or `:admin`. Additional credential overrides may be supplied via `overrides`. """ @spec client(interface(), map()) :: Tesla.Client.t() def client(interface \\ :base, overrides \\ %{}) do url = interface_url(interface) overrides = Map.new(overrides) admin = Environment.server_admin() proxy_defaults = Environment.proxy_config() credentials = %{ username: Map.get(overrides, :username, admin.user), password: Map.get(overrides, :password, admin.password), bearer_token: Map.get(overrides, :bearer_token, Environment.bearer_token()), proxy_user: Map.get(overrides, :proxy_user, proxy_defaults.user), proxy_roles: Map.get(overrides, :proxy_roles, proxy_defaults.roles), proxy_secret: Map.get(overrides, :proxy_secret, proxy_defaults.secret), proxy_token: Map.get(overrides, :proxy_token, proxy_defaults.token), url: url } |> Map.update(:proxy_roles, [], &normalize_roles/1) |> Map.merge( Map.drop(overrides, [ :username, :password, :bearer_token, :proxy_user, :proxy_roles, :proxy_secret, :proxy_token ]) ) middleware = [ {Middleware.BaseUrl, url}, {Middleware.Headers, @default_headers}, {Middleware.JSON, engine: Jason} ] ++ auth_middleware(Environment.auth_type(), credentials, url) Tesla.client(middleware, @default_adapter) end @doc """ Convenience wrapper around `client/2` for admin or cluster endpoints. """ @spec admin_client(map()) :: Tesla.Client.t() def admin_client(overrides \\ %{}) do client(:admin, overrides) end @doc """ Executes an HTTP request using either the provided `%Tesla.Client{}` or a freshly built one. """ @spec request(atom(), String.t(), keyword()) :: {:ok, Tesla.Env.t()} | {:error, any()} def request(method, url, opts \\ []) do {client, opts} = pop_client(opts) request_opts = opts |> Keyword.put(:method, method) |> Keyword.put(:url, url) Tesla.request(client, request_opts) end @doc """ Executes an HTTP request against the admin interface. """ @spec admin_request(atom(), String.t(), keyword()) :: {:ok, Tesla.Env.t()} | {:error, any()} def admin_request(method, url, opts \\ []) do opts = Keyword.put(opts, :interface, :admin) request(method, url, opts) end for verb <- ~w(delete get head options patch post put trace)a do @spec unquote(verb)(String.t(), keyword()) :: {:ok, Tesla.Env.t()} | {:error, any()} def unquote(verb)(url, opts \\ []) do request(unquote(verb), url, opts) end @spec unquote(:"admin_#{verb}")(String.t(), keyword()) :: {:ok, Tesla.Env.t()} | {:error, any()} def unquote(:"admin_#{verb}")(url, opts \\ []) do admin_request(unquote(verb), url, opts) end end defp pop_client(opts) do case Keyword.pop(opts, :client) do {nil, rest_opts} -> {interface, rest_opts} = Keyword.pop(rest_opts, :interface, :base) overrides = rest_opts |> Keyword.get(:credentials, %{}) |> Map.new() rest_opts = Keyword.drop(rest_opts, [:credentials]) {client(interface, overrides), rest_opts} {client, rest_opts} -> {client, rest_opts} end end defp interface_url(:backend), do: Environment.backend_url() defp interface_url(:admin), do: Environment.admin_url() defp interface_url(_), do: Environment.base_url() defp auth_middleware(:admin_party, _opts, _url), do: [] defp auth_middleware(:basic, opts, _url) do [{Tesla.Middleware.CouchDB.BasicAuth, opts}] end defp auth_middleware(:cookie, opts, url) do [{Tesla.Middleware.CouchDB.CookieAuth, Map.put(opts, :url, url)}] end defp auth_middleware(:oauth, opts, url) do credentials = Environment.oauth_creds() |> Map.merge(%{url: url}) |> Map.merge(opts) [{Tesla.Middleware.CouchDB.OAuth, credentials}] end defp auth_middleware(:bearer, opts, _url) do [{Tesla.Middleware.CouchDB.BearerAuth, opts}] end defp auth_middleware(:proxy, opts, _url) do [{Tesla.Middleware.CouchDB.ProxyAuth, opts}] end defp auth_middleware(_, _opts, _url), do: [] defp normalize_roles(nil), do: [] defp normalize_roles(roles) when is_list(roles) do roles |> Enum.map(&to_string/1) |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == "")) end defp normalize_roles(roles) when is_binary(roles) do roles |> String.split([",", " "], trim: true) |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == "")) end defp normalize_roles(other), do: normalize_roles([other]) end