lib/chalk/client.ex

defmodule Chalk.Client do
  @version Chalk.Mixfile.project()[:version]

  @spec new(map) :: Tesla.Client.t()
  def new(config \\ %{}) do
    middleware =
      get_base_middleware(config) ++
        get_authentication_middleware(config) ++ get_middleware(config)

    adapter = {get_adapter(config), get_http_options(config)}

    Tesla.client(middleware, adapter)
  end

  # HELPERS

  defp get_base_url(config) do
    config[:api_server] || "https://api.chalk.ai/"
  end

  defp get_base_middleware(config) do
    deployment_id_header =
      case get_deployment_id(config) do
        id when not is_nil(id) ->
          [{"x-chalk-preview-deployment", id}]

        _ ->
          []
      end

    [
      {Tesla.Middleware.BaseUrl, get_base_url(config)},
      {Tesla.Middleware.Headers,
       [
         {"Content-Type", "application/json"},
         {"user-agent", "chalk-elixir v#{@version}"}
         | deployment_id_header
       ]},
      Tesla.Middleware.JSON
    ]
  end

  defp get_authentication_middleware(config) do
    unauthenticated? = Map.get(config, :unauthenticated, false)

    unless unauthenticated? do
      [
        {Chalk.Tesla.CredentialsMiddleware,
         %{
           client_id: get_client_id(config),
           client_secret: get_client_secret(config),
           api_server: get_base_url(config),
           deployment_id: get_deployment_id(config)
         }}
      ]
    else
      []
    end
  end

  defp get_middleware(config) do
    case config[:middleware] || [] do
      middleware when is_list(middleware) ->
        middleware

      m ->
        [m]
    end
  end

  defp get_client_id(config) do
    Map.get(config, "client_id", System.get_env("CHALK_CLIENT_ID"))
  end

  defp get_client_secret(config) do
    Map.get(config, "client_secret", System.get_env("CHALK_CLIENT_SECRET"))
  end

  defp get_deployment_id(config) do
    Map.get(config, "deployment_id", System.get_env("DEPLOYMENT_ID"))
  end

  defp get_adapter(config) do
    config[:adapter] || Application.get_env(:chalk, :adapter) || Tesla.Adapter.Hackney
  end

  defp get_http_options(config) do
    Keyword.merge(
      Application.get_env(:chalk, :http_options, []),
      config[:http_options] || []
    )
  end

  defmodule Request do
    @moduledoc """
    Data structure for an HTTP request with convenience functions.
    """

    @derive Jason.Encoder
    defstruct body: %{}, endpoint: nil, method: nil, opts: %{}
    @type t :: %__MODULE__{body: map, endpoint: String.t(), method: atom, opts: map}

    @doc """
    Convert `Request` to `options` format passed to `Tesla.request/2`.
    """
    @spec to_options(Request.t()) :: keyword
    def to_options(%Request{body: b, endpoint: e, method: m, opts: o}) do
      [method: m, url: e, body: b, opts: Map.to_list(o)]
    end

    @doc """
    Add telemetry metadata to `Request`.
    Calling without the second argument adds default metadata. Custom metadata
    is added by passing a map with a key `telemetry_metadata`.
    Example
    ```
    Request.add_metadata(request, %{telemetry_metadata: %{k: v}})
    ```
    """
    @spec add_metadata(Request.t()) :: Request.t()
    @spec add_metadata(Request.t(), map) :: Request.t()
    def add_metadata(%Request{endpoint: e, method: m, opts: o} = request, config \\ %{}) do
      metadata =
        Map.new()
        |> Map.put(:method, m)
        |> Map.put(:path, e)
        |> Map.put(:u, :native)
        |> Map.merge(config[:telemetry_metadata] || %{})

      %{request | opts: Map.put(o, :metadata, metadata)}
    end
  end
end