Parameter

Parameter is a library for dealing with complex datatypes by solving the following problems:

Motivation

Offer a similar Schema model from Ecto library to deal with complex data schemas. The main use case is to parse response from external apis. Parameter provides a well structured schema model which tries it's best to parse the external data.

Schema

The first step for building a schema for your data is to create a schema definition. This can be achieved by using the Parameter.Schema macro.

defmodule UserSchema do
use Parameter.Schema
param do
field :first_name, :string, key: "firstName", required: true
field :last_name, :string, key: "lastName", required: true, default: ""
field :age, :integer
has_one :main_address, AddressSchema, key: "mainAddress", required: true
has_many :addresses, AddressSchema
end
end

And the AddressSchema:

defmodule AddressSchema do
use Parameter.Schema
param do
field :city, :string, required: true
field :street, :string
field :number, :integer
end
end

It's also possible to avoid creating modules for nested types by using the following api:

defmodule UserSchema do
use Parameter.Schema
param do
field :first_name, :string, key: "firstName", required: true
field :last_name, :string, key: "lastName", required: true, default: ""
field :age, :integer
has_one :main_address, AddressSchema, key: "mainAddress", required: true do
field :city, :string, required: true
field :street, :string
field :number, :integer
end
end
end

Another possibility is avoiding creating files for a schema at all. This can be done by importing Parameter.Schema and using the param/2 macro. This is useful for adding params in Phoenix controllers. For example:

defmodule MyProjectWeb.UserController do
use MyProjectWeb, :controller
import Parameter.Schema
alias MyProject.Users
param UserParams do
field :first_name, :string, required: true
field :last_name, :string, required: true
end
def create(conn, params) do
with {:ok, user_params} <- Parameter.load(UserParams, params),
{:ok, user} <- Users.create_user(user_params) do
render(conn, "user.json", %{user: user})
end
end

Each field needs to define the type that will be parsed and the options (if any). The available types are:

* Any module that implements the Parameter.Parametrizable behaviour is eligible to be a field in the schema definition.

The options available for the field definition are:

After the definition, the schema can be validated and parsed against external parameters using the Parameter.load/3 function.

Data Deserialization

This is a common requirement when receiving data from an external source that needs validation and deserialization of data to an Elixir definition. This can be achieved using Parameter.load/2 or Parameter.load/3 functions:

iex> params = %{
"mainAddress" => %{"city" => "New York"},
"addresses" => [%{"city" => "Rio de Janeiro"}],
"age" => "32",
"firstName" => "John",
"lastName" => "Doe"
}
...> Parameter.load(UserSchema, params)
{:ok,
%{
addresses: [%{city: "Rio de Janeiro"],
age: 32,
first_name: "John",
last_name: "Doe",
main_address: %{city: "New York"}
}}

Adding invalid data should return validation errors:

iex> params = %{
"mainAddress" => %{"city" => "New York", "number" => "123AB"},
"addresses" => [
%{
"city" => "New York",
"number" => "123AB"
},
%{
"city" => "Rio de Janeiro",
"number" => "Not number"
}
],
"age" => "AA",
"lastName" => "Doe"
}
...> Parameter.load(UserSchema, params)
{:error,
%{
addresses: [
"0": %{number: "invalid integer type"},
"1": %{number: "invalid integer type"}
],
age: "invalid integer type",
first_name: "is required",
main_address: %{number: "invalid integer type"}
}}

Custom Types

For implementing custom types create a module that implements the Parameter.Parametrizable behaviour.

Check the following example on how Integer parameter was implemented:

defmodule IntegerCustomType do
@moduledoc """
Integer parameter type
"""
@behaviour Parameter.Parametrizable
@impl true
# `load/1` is evaluated when parsing the parameters, you can do validations here and transform the data
def load(value) when is_integer(value) do
{:ok, value}
end
def load(value) when is_binary(value) do
case Integer.parse(value) do
{integer, ""} -> {:ok, integer}
_error -> error_tuple()
end
end
def load(_value) do
{:error, "invalid integer type"}
end
@impl true
# `validate/1` checks the schema during compile time. It verifies the default value if it's passed to the schema validating its type
def validate(value) when is_integer(value) do
:ok
end
def validate(_value) do
{:error, "invalid integer type"}
end
end

Add the custom module on the schema definition:

defmodule UserSchema do
use Parameter.Schema
param do
field :age, IntegerCustomType, required: true
end
end

Unknown fields

Loading will ignore fields that does not have a matching key in the schema. This behaviour can be changed with the following options:

Using the same user schema, adding unknow field option to error should return an error:

iex> params = %{"user_token" => "3hgj81312312"}
...> Parameter.load(UserSchema, params, unknown_field: :error)
{:error, %{"user_token" => "unknown field"}}

Installation

Add parameter to your list of dependencies in mix.exs:

def deps do
[
{:parameter, "~> 0.3.0"}
]
end

Also for add this depedency inside .formatter.exs file:

import_deps: [:ecto, :phoenix, :parameter],

Roadmap