lib/jsonapi_plug/api.ex

defmodule JSONAPIPlug.API do
  @moduledoc """
    JSON:API API Configuration

    You can define an API by "use-ing" `JSONAPIPlug.API` in your API module:

    ```elixir
    defmodule MyApp.API do
      use JSONAPIPlug.API, otp_app: :my_app
    end
    ```

    API module configuration can be customized via your application configuration:

    ```elixir
    config :my_app, MyApp.API,
      namespace: "api",
      case: :dasherize
    ```

    See `t:options/0` for all available configuration options.
  """

  @options_schema [
    otp_app: [
      doc: "OTP application to use for API configuration.",
      type: :atom,
      required: true
    ]
  ]

  @config_schema [
    case: [
      doc:
        "This option controls how your API's field names will be cased. The current `JSON:API Specification v1.0` recommends dasherizing (e.g. `\"favorite-color\": \"blue\"`), while the upcoming `JSON:API Specification v1.1` recommends camelCase (e.g. `\"favoriteColor\": \"blue\"`).",
      type: {:in, [:camelize, :dasherize, :underscore]},
      default: :camelize
    ],
    client_generated_ids: [
      doc:
        "Enable support for Client-Generated IDs. When enabled, the resources received in requests are supposed to contain a valid 'id'.",
      type: :boolean,
      default: false
    ],
    host: [
      doc: "Hostname used for link generation instead of deriving it from the connection.",
      type: :string
    ],
    namespace: [
      doc:
        "Namespace for all resources in your API. if you want your resources to live under \".../api/v1\", pass `namespace: \"api/v1\"`.",
      type: :string
    ],
    normalizer: [
      doc: "Normalizer for transformation of `JSON:API` document to and from user data.",
      type: :atom,
      default: JSONAPIPlug.Normalizer.Ecto
    ],
    query_parsers: [
      doc: "Parsers for transformation of `JSON:API` request query parameters to user data.",
      type: :keyword_list,
      keys: [
        fields: [
          doc: "Fields query parameter parser.",
          type: :atom,
          default: JSONAPIPlug.QueryParser.Ecto.Fields
        ],
        filter: [
          doc: "Filter query parameter parser.",
          type: :atom,
          default: JSONAPIPlug.QueryParser.Filter
        ],
        include: [
          doc: "Include query parameter parser.",
          type: :atom,
          default: JSONAPIPlug.QueryParser.Ecto.Include
        ],
        page: [
          doc: "Page query parameter parser.",
          type: :atom,
          default: JSONAPIPlug.QueryParser.Page
        ],
        sort: [
          doc: "Sort query parameter parser.",
          type: :atom,
          default: JSONAPIPlug.QueryParser.Ecto.Sort
        ]
      ],
      default: [
        fields: JSONAPIPlug.QueryParser.Ecto.Fields,
        filter: JSONAPIPlug.QueryParser.Filter,
        include: JSONAPIPlug.QueryParser.Ecto.Include,
        page: JSONAPIPlug.QueryParser.Page,
        sort: JSONAPIPlug.QueryParser.Ecto.Sort
      ]
    ],
    pagination: [
      doc: "A module adopting the `JSONAPIPlug.Pagination` behaviour for pagination.",
      type: :atom,
      default: nil
    ],
    port: [
      doc: "Port used for link generation instead of deriving it from the connection.",
      type: :pos_integer
    ],
    scheme: [
      doc: "Scheme used for link generation instead of deriving it from the connection.",
      type: {:in, [:http, :https]}
    ],
    version: [
      doc: "`JSON:API` version advertised in the document",
      type: {:in, [:"1.0"]},
      default: :"1.0"
    ]
  ]

  @type t :: module()

  @typedoc """
  API configuration options:

  #{NimbleOptions.docs(@config_schema)}
  """
  @type options :: keyword()

  defmacro __using__(options) do
    otp_app =
      options
      |> NimbleOptions.validate!(@options_schema)
      |> Keyword.fetch!(:otp_app)

    quote do
      @doc false
      def __otp_app__, do: unquote(otp_app)
    end
  end

  @doc """
  Retrieve a configuration option

  Retrieves an API configuration option value, with fallback to a default value
  in case the configuration option is not present.
  """
  @spec get_config(t() | nil, [atom()], term()) :: term()
  def get_config(api, path, default \\ nil)

  def get_config(nil = _api, _path, default), do: default

  def get_config(api, path, default) do
    api
    |> get_all_config()
    |> NimbleOptions.validate!(@config_schema)
    |> get_in(path) || default
  end

  defp get_all_config(api) do
    api.__otp_app__()
    |> Application.get_env(api, [])
  end
end