lib/formular/client/config.ex

defmodule Formular.Client.Config do
  @moduledoc """
  Configuration for a Formular client.

  The following keys are currently supported:

  - `client_name`
    - the identity of the client, can be any string. Default `nil`
  - `url`
    - the url of the remote server, default `nil`
  - `read_timeout`
    - a timeout setting for waiting for all the formulas
  - `formulas`
    - formula definition, check "Formula Configuration" section for more
      information.
  - `compiler`
    - MFA config for compiling the string into Elixir code
  - `adapter`
    - an adapter for getting the data from the remote server

  ## Formula Configuration

  A fomular is configured as a two-element tuple:

  ```elixir
  {"my-formula", compile_as: MyFm, context: My.ContextModule}
  ```

  Here `MyFm` is the name of module for the code to be compiled into;
  `"my-formula"` is the name of the formula on the server;
  `My.ContextModule` is the helper module that can be used in the 
  code.
  ```

  A single formula name string is also accepted:

  ```elixir
  [
    ...
    formulas: ["my-formula"]
  ]
  ```

  In this case, `compile_as` and `context` are optional and treated as
  `nil`s.
  """
  alias __MODULE__

  require Logger

  @type formula_name() :: String.t()
  @type formula_def() :: {formula_name(), compile_options()}
  @type compile_options() :: [compile_option()]
  @type compile_option() ::
          {:compile_as, module()} | {:context, module()} | {:allow_modules, [module()]}
  @type formula_full_def() ::
          formula_def()
          | {module(), name :: String.t(), module() | nil}
          | {module(), name :: String.t()}

  @type compile_function ::
          ({name :: String.t(), code :: binary(), module(), nil | module()} ->
             :ok | {:error, term()})

  @type t :: %Config{
          client_name: String.t(),
          url: String.t(),
          read_timeout: :infinity | non_neg_integer(),
          formulas: [formula_def()],
          compiler: {module(), atom(), args :: list()} | compile_function(),
          adapter: {module(), keyword()}
        }

  defstruct [
    :client_name,
    :url,
    formulas: [],
    read_timeout: 10_000,
    compiler: {Formular.Client.Compiler, :compile, []},
    adapter: {Formular.Client.Adapter.Websocket, []}
  ]

  @doc """
  Build a new config constructure.
  """
  @spec new(Enum.t()) :: t()
  def new(opts) do
    struct!(Config, opts)
    |> format_formulas()
  end

  defp format_formulas(config) do
    config
    |> Map.update!(
      :formulas,
      &Enum.map(&1 || [], fn
        name when is_binary(name) ->
          {name, []}

        {mod, name} when is_atom(mod) and is_binary(name) ->
          Logger.warning(
            "This format ({module, formula_name}) of formula Configuration is deprecated, please use `{formula_name, compile_as: module}` format."
          )

          {name, compile_as: mod}

        {name, opts} when is_binary(name) and is_list(opts) ->
          {name, opts}

        {mod, name, context} when is_atom(mod) and is_binary(name) and is_atom(context) ->
          Logger.warning(
            "This format ({module, formula_name, context}) of formula Configuration is deprecated, please use `{formula_name, compile_as: module, context: context}` format."
          )

          {name, compile_as: mod, context: context}
      end)
    )
  end

  @doc """
  Get the config for a given formula name.
  """
  @spec formula_config(t(), formula_name()) :: formula_def() | nil
  def formula_config(%Config{formulas: formulas}, name) when is_list(formulas) do
    Enum.find(formulas, fn
      {^name, _opts} ->
        true

      _ ->
        false
    end)
  end
end