lib/parameter.ex

defmodule Parameter do
  @moduledoc """
  `Parameter` is a library for dealing with complex datatypes by solving the following problems:
  - Schema creation and validation
  - Input data validation
  - Deserialization
  - Serialization

  ## Example

  Create a schema

      defmodule UserParam do
        use Parameter.Schema
        alias Parameter.Validators

        param do
          field :first_name, :string, key: "firstName", required: true
          field :last_name, :string, key: "lastName"
          field :email, :string, validator: &Validators.email(&1)
          has_one :address, Address do
            field :city, :string, required: true
            field :street, :string
            field :number, :integer
          end
        end
      end

  Load (deserialize) the schema against external parameters:

      params = %{
        "firstName" => "John",
        "lastName" => "Doe",
        "email" => "john@email.com",
        "address" => %{"city" => "New York", "street" => "York"}
      }
      Parameter.load(User, params)
      {:ok, %{
        first_name: "John",
        last_name: "Doe",
        email: "john@email.com",
        main_address: %{city: "New York", street: "York"}
      }}

  or Dump (serialize) a populated schema to params:

      schema = %{
          first_name: "John",
          last_name: "Doe",
          email: "john@email.com",
          main_address: %{city: "New York", street: "York"}
        }
      Parameter.dump(User, params)
      {:ok,
      %{
          "firstName" => "John",
          "lastName" => "Doe",
          "email" => "john@email.com",
          "address" => %{"city" => "New York", "street" => "York"}
      }}

  For more schema options checkout `Parameter.Schema`
  """

  alias Parameter.Dumper
  alias Parameter.Loader
  alias Parameter.Types

  @unknown_opts [:error, :ignore]

  @spec load(module() | atom(), map(), Keyword.t()) :: {:ok, any()} | {:error, any()}
  def load(schema, input, opts \\ []) do
    opts = parse_opts(opts)
    Loader.load(schema, input, opts)
  end

  @spec dump(module() | atom(), map(), Keyword.t()) :: {:ok, any()} | {:error, any()}
  def dump(schema, input, opts \\ []) when is_map(input) do
    exclude = Keyword.get(opts, :exclude, [])
    Types.validate!(:list, exclude)
    Dumper.dump(schema, input, exclude: exclude)
  end

  defp parse_opts(opts) do
    unknown = Keyword.get(opts, :unknown, :ignore)

    if unknown not in @unknown_opts do
      raise("unknown field options should be #{inspect(@unknown_opts)}")
    end

    struct = Keyword.get(opts, :struct, false)

    Types.validate!(:boolean, struct)

    exclude = Keyword.get(opts, :exclude, [])

    Types.validate!(:list, exclude)

    [struct: struct, unknown: unknown, exclude: exclude]
  end
end