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 ## Examples 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, AddressParam 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(UserParam, params) {:ok, %{ first_name: "John", last_name: "Doe", email: "john@email.com", 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", address: %{city: "New York", street: "York"} } Parameter.dump(UserParam, 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] @doc """ Loads parameters into the given schema. ## Options * `:struct` - If set to `true` loads the schema into a structure. If `false` (default) loads with plain maps. * `:unknown` - Defines the behaviour when unknown fields are presented on the parameters. The options are `:ignore` (default) or `:error`. * `:exclude` - Accepts a list of fields to be excluded when loading the parameters. This option is useful if you have fields in your schema are only for dump. The field will not be checked for any validation if it's on the exclude list. * `:many` - When `true` will parse the input data as list, when `false` (default) it parses as map ## Examples defmodule UserParam do use Parameter.Schema param do field :first_name, :string, key: "firstName", required: true field :last_name, :string, key: "lastName" has_one :address, Address do field :city, :string, required: true field :street, :string field :number, :integer end end end params = %{ "address" => %{"city" => "New York", "street" => "broadway"}, "firstName" => "John", "lastName" => "Doe" } Parameter.load(UserParam, params) {:ok, %{ first_name: "John", last_name: "Doe", address: %{city: "New York", street: "broadway"} }} # Using struct options Parameter.load(UserParam, params, struct: true) {:ok, %UserParam{ first_name: "John", last_name: "Doe", address: %AddressParam{city: "New York", street: "broadway"} }} # Excluding fields Parameter.load(UserParam, params, exclude: [:first_name, {:address, [:city]}]) {:ok, %{ last_name: "Doe", address: %{street: "broadway"} }} # Unknown fields should return errors params = %{"user_token" => "3hgj81312312"} Parameter.load(UserParam, params, unknown: :error) {:error, %{"user_token" => "unknown field"}} # Invalid data should return validation errors: params = %{ "address" => %{"city" => "New York", "number" => "123AB"}, "lastName" => "Doe" } Parameter.load(User, params) {:error, %{ first_name: "is required", address: %{number: "invalid integer type"}, }} """ @spec load(module() | atom(), map() | list(map()), Keyword.t()) :: {:ok, any()} | {:error, any()} def load(schema, input, opts \\ []) do opts = parse_opts(opts) Loader.load(schema, input, opts) end @doc """ Dump the loaded parameters. ## Options * `:exclude` - Accepts a list of fields to be excluded when dumping the loaded parameter. This option is useful if you have fields in your schema are only for loading. * `:many` - When `true` will parse the input data as list, when `false` (default) it parses as map ## Examples defmodule UserParam do use Parameter.Schema param do field :first_name, :string, key: "firstName", required: true field :last_name, :string, key: "lastName" has_one :address, Address do field :city, :string, required: true field :street, :string field :number, :integer end end end loaded_params = %{ first_name: "John", last_name: "Doe", address: %{city: "New York", street: "broadway"} } Parameter.dump(UserParam, params) {:ok, %{ "address" => %{"city" => "New York", "street" => "broadway"}, "firstName" => "John", "lastName" => "Doe" }} # excluding fields Parameter.dump(UserParam, params, exclude: [:first_name, {:address, [:city]}]) {:ok, %{ "address" => %{"street" => "broadway"}, "lastName" => "Doe" }} """ @spec dump(module() | atom(), map() | list(map), Keyword.t()) :: {:ok, any()} | {:error, any()} def dump(schema, input, opts \\ []) do exclude = Keyword.get(opts, :exclude, []) many = Keyword.get(opts, :many, false) Types.validate!(:list, exclude) Types.validate!(:boolean, many) Dumper.dump(schema, input, exclude: exclude, many: many) 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) exclude = Keyword.get(opts, :exclude, []) many = Keyword.get(opts, :many, false) Types.validate!(:boolean, struct) Types.validate!(:list, exclude) Types.validate!(:boolean, many) [struct: struct, unknown: unknown, exclude: exclude, many: many] end end