lib/castor_edc.ex

defmodule CastorEDC do
  @moduledoc ~S"""
  Facilitates authentication against the Castor EDC API

      alias CastorEDC.Client

      client = Client.new(%{client_id: "<client id>", client_secret: "<client secret>"})
      {:ok, client} = CastorEDC.authenticate(client)

  See `CastorEDC.Client.new/2` for all the options.

  After authenticating against the API you can then use the other modules to perform
  API requests, e.g:

      {200, list_of_studies, _} = CastorEDC.Common.Studies.list(client)

      list_of_studies
  """

  alias CastorEDC.Client
  alias Jason

  @doc """
  Exchanges a valid client id & secret combination for an access token that can be used to
  make further API calls.
  """
  @spec authenticate(Client.t()) :: {:ok, Client.t()} | {:error, String.t()}
  def authenticate(%Client{options: opts} = client) do
    [
      {Tesla.Middleware.FormUrlencoded, []},
      {Tesla.Middleware.Opts, adapter: opts[:adapter_options]},
      {Tesla.Middleware.Headers, [{"User-Agent", opts[:user_agent]}]}
    ]
    |> http_client(client)
    |> Tesla.post(
      url(client, "oauth/token"),
      %{
        client_id: client.client_id,
        client_secret: client.client_secret,
        grant_type: client.grant_type,
        scope: client.scope
      }
    )
    |> handle_authentication_response(client)
  end

  @doc false
  @spec post(String.t(), Client.t(), %{}) :: {integer(), %{}, Tesla.Env.t()}
  def post(url, %Client{} = client, body) do
    default_middleware(client)
    |> http_client(client)
    |> Tesla.post(url(client, url), body)
    |> handle_response()
  end

  @doc false
  @spec patch(String.t(), Client.t(), %{}) :: {integer(), %{}, Tesla.Env.t()}
  def patch(url, %Client{} = client, body) do
    default_middleware(client)
    |> http_client(client)
    |> Tesla.patch(url(client, url), body)
    |> handle_response()
  end

  @doc false
  @spec get(String.t(), Client.t(), []) :: {integer(), %{}, Tesla.Env.t()}
  def get(url, %Client{} = client, params \\ []) do
    default_middleware(client)
    |> http_client(client)
    |> Tesla.get(url(client, url), query: params)
    |> handle_response()
  end

  defp handle_authentication_response({:error, reason}, _client), do: {:error, reason}

  defp handle_authentication_response({:ok, %Tesla.Env{body: body, status: 200}}, client) do
    access_token = Jason.decode!(body)["access_token"]
    {:ok, Client.update_token(client, access_token)}
  end

  defp handle_authentication_response({_, %Tesla.Env{body: body}}, _) do
    error_description = Jason.decode!(body)["error_description"]
    {:error, error_description}
  end

  defp default_middleware(%Client{access_token: token, options: opts}) do
    middleware = [
      {Tesla.Middleware.JSON, []},
      {Tesla.Middleware.BearerAuth, token: token},
      {Tesla.Middleware.Headers,
       [{"Accept", "application/json"}, {"User-Agent", opts[:user_agent]}]}
    ]

    # If we're using the Tesla.Mock adapter we skip adding the timeout middleware
    # https://github.com/elixir-tesla/tesla/issues/157
    if opts[:adapter] === Tesla.Mock do
      middleware
    else
      [{Tesla.Middleware.Timeout, timeout: opts[:global_timeout]} | middleware]
    end
  end

  defp http_client(middleware, %Client{options: opts}) do
    Tesla.client(middleware, {opts[:adapter], opts[:adapter_options]})
  end

  defp process_response_body(""), do: nil
  defp process_response_body(body) when is_binary(body), do: Jason.decode!(body)
  defp process_response_body(body), do: body

  defp handle_response({:error, reason}), do: {:error, reason}

  defp handle_response({:ok, %Tesla.Env{status: status, body: body} = response}) do
    {status, process_response_body(body), response}
  end

  defp url(%Client{endpoint: endpoint}, path), do: endpoint <> path
end