lib/cozy_oss/api_client.ex

defmodule CozyOSS.ApiClient do
  @moduledoc """
  Specification for a CozyOSS API client.

  It can be set to a client provided by CozyOSS, such as:

      config :cozy_oss, :api_client, CozyOSS.ApiClient.Finch

  Or, set it to your own API client, such as:

      config :cozy_oss, :api_client, MyApiClient

  """

  alias CozyOSS.ApiRequest
  alias CozyOSS.XML

  @type status :: pos_integer()
  @type headers :: [{binary(), binary()}]
  @type body :: binary()

  @typedoc """
  The response of a request.
  """
  @type response() :: {:ok, status, headers, body} | {:error, term()}

  @doc """
  Callback to initialize the given API client.
  """
  @callback init() :: :ok

  @doc """
  Callback to send a request.
  """
  @callback request(ApiRequest.t()) :: response()

  @optional_callbacks init: 0

  @doc false
  def init do
    client = api_client()

    if Code.ensure_loaded?(client) and function_exported?(client, :init, 0) do
      :ok = client.init()
    end

    :ok
  end

  @doc """
  Send a struct `%CozyOSS.ApiRequest{}` as an HTTP request by the given API client.

  When the `Content-Type` header of the response is `"application/xml"`, this function will try to convert
  the XML content to a map with snaked-cased keys.

  """
  @spec request(ApiRequest.t()) :: response()
  def request(%ApiRequest{} = req) do
    req
    |> api_client().request()
    |> maybe_to_map()
  end

  defp maybe_to_map({:ok, status, headers, body} = response) do
    case List.keyfind(headers, "content-type", 0) do
      {"content-type", "application/xml"} ->
        {:ok, status, headers, XML.to_map!(body)}

      _ ->
        response
    end
  end

  defp maybe_to_map(response), do: response

  defp api_client do
    Application.fetch_env!(:cozy_oss, :api_client)
  end
end