lib/cozy_oss/api_spec.ex

defmodule CozyOSS.ApiSpec do
  @moduledoc """
  Describes the specification of an API.
  """

  @enforce_keys [
    :bucket,
    :object,
    :sub_resources,
    :method,
    :path,
    :query,
    :headers,
    :body
  ]
  defstruct bucket: nil,
            object: nil,
            sub_resources: %{},
            method: nil,
            path: nil,
            query: %{},
            headers: %{},
            body: nil

  @typedoc """
  API method.
  """
  @type method() :: String.t()

  @typedoc """
  API path.
  """
  @type path() :: String.t()

  @typedoc """
  API query.
  """
  @type query() :: %{
          optional(query_name :: String.t()) => query_value :: boolean() | number() | String.t()
        }

  @typedoc """
  API headers.
  """
  @type headers() :: %{optional(header_name :: String.t()) => header_value :: String.t()}

  @typedoc """
  Optional API body.
  """
  @type body() :: iodata() | nil

  @type config() :: map()

  @type t :: %__MODULE__{
          method: method(),
          path: path(),
          query: query(),
          headers: headers(),
          body: body()
        }

  @spec build!(config()) :: t()
  def build!(config) when is_map(config) do
    config
    |> validate_required_keys!()
    |> normalize_config!()
    |> as_struct!()
  end

  defp validate_required_keys!(
         %{
           method: method,
           path: path
         } = config
       )
       when is_binary(method) and is_binary(path) do
    config
  end

  defp validate_required_keys!(_config) do
    raise ArgumentError,
          "key :method, :path are required in a spec"
  end

  defp normalize_config!(config) do
    Map.update!(config, :path, &add_prefix_slash/1)
  end

  defp add_prefix_slash(path) do
    Path.join("/", path)
  end

  defp as_struct!(config) do
    default_struct = __MODULE__.__struct__()
    valid_keys = Map.keys(default_struct)
    config = Map.take(config, valid_keys)
    Map.merge(default_struct, config)
  end
end