View Source Normalizer (normalizer v0.3.0)

Normalizer

Normalizes string-keyed maps to atom-keyed maps while converting values according to a given schema. Particularly useful when working with param maps.

Usage

schema = %{
  user_id: {:number, required: true},
  name: :string,
  admin: {:boolean, default: false},
  languages: [:string]
}

params = %{
  "user_id" => "42",
  "name" => "Neo",
  "languages" => ["en"],
  "age" => "55"
}

> Normalizer.normalize(params, schema)
%{
  user_id: 42,
  name: "Neo",
  admin: false,
  languages: ["en"]
}

Properties

  • Converts types whenever possible and reasonable;
  • Ensures required values are given;
  • Filters keys and values not in the schema;
  • Supports basic types, lists, maps, and nested lists/maps.

See Normalizer.normalize/2 for more details.

Link to this section Summary

Functions

Normalizes a string-map using the given schema.

Link to this section Types

Specs

schema() :: %{required(atom()) => value_schema()}

Specs

value_options() :: [required: boolean(), default: any(), with_offset: boolean()]

Specs

value_schema() :: value_type() | {value_type(), value_options()}

Specs

value_type() :: atom() | [atom()] | map()

Link to this section Functions

Link to this function

normalize(params, schema)

View Source

Specs

normalize(params :: %{required(String.t()) => any()}, schema :: schema()) ::
  {:ok, %{required(atom()) => any()}} | {:error, String.t()}

Normalizes a string-map using the given schema.

The schema is expected to be a map where each key is an atom representing an expected string key in params, pointing to the type to which the respective value in the params should be normalized.

The return is a normalized map, in case of success, or a map with each erroring key and a description, in case of failure.

Types

Types can be one of:

  • Primitives: :string, :number, :boolean.
  • Parseable values: :datetime, :date.
  • Maps: a nested schema for a nested map.
  • Lists: one-element lists that contain any of the other types.
  • Tuples: two-element tuples where the first element is one of the other types, and the second element a keyword list of options.

Primitives

Strings are somewhat of a catch all that ensures anything but lists and maps are converted to strings:

iex> Normalizer.normalize(%{"key" => 42}, %{key: :string})
{:ok, %{key: "42"}}

Numbers are kept as is, if possible, or parsed:

iex> Normalizer.normalize(%{"key" => 42}, %{key: :number})
{:ok, %{key: 42}}

iex> Normalizer.normalize(%{"key" => "42.5"}, %{key: :number})
{:ok, %{key: 42.5}}

Booleans accept native values, "true" and "false" strings, and "1" and "0" strings:

iex> Normalizer.normalize(%{"key" => true}, %{key: :boolean})
{:ok, %{key: true}}

iex> Normalizer.normalize(%{"key" => "false"}, %{key: :boolean})
{:ok, %{key: false}}

Parseable Values

Only :datetime and :date are supported right now.

iex> Normalizer.normalize(%{"key" => "2020-02-11T00:00:00+0100"}, %{key: :datetime})
{:ok, %{key: ~U[2020-02-10T23:00:00Z]}}

iex> Normalizer.normalize(%{"key" => "2020-02-11"}, %{key: :date})
{:ok, %{key: ~D[2020-02-11]}}

The offset can be extracted as well by passing the with_offset option:

iex> Normalizer.normalize(
...>   %{"key" => "2020-02-11T00:00:00+0100"},
...>   %{key: {:datetime, with_offset: true}}
...> )
{:ok, %{key: {~U[2020-02-10T23:00:00Z], 3600}}}

Maps

Nested schemas are supported:

iex> Normalizer.normalize(
...>   %{"key" => %{"age" => "42"}},
...>   %{key: %{age: :number}}
...> )
{:ok, %{key: %{age: 42}}}

Lists

Lists are represented in the schema by a single-element list:

iex> Normalizer.normalize(
...>   %{"key" => ["42", 52]},
...>   %{key: [:number]}
...> )
{:ok, %{key: [42, 52]}}

We can normalize lists of any one of the other types.

Options

Per-value options can be specified by passing a two-element tuple in the type specification. The three available options are :required, :default, and :with_offset.

:required fails the validation process if the key is missing or nil:

iex> Normalizer.normalize(%{"key" => nil}, %{key: :number})
{:ok, %{key: nil}}

iex> Normalizer.normalize(%{"key" => nil}, %{key: {:number, required: true}})
{:error, %{key: "required number, got nil"}}

iex> Normalizer.normalize(%{}, %{key: {:number, required: true}})
{:error, %{key: "required number"}}

:default ensures a value in a given key, if nil or missing:

iex> Normalizer.normalize(%{}, %{key: {:number, default: 42}})
{:ok, %{key: 42}}

iex> Normalizer.normalize(%{"key" => 24}, %{key: {:number, default: 42}})
{:ok, %{key: 24}}

:with_offset is explained in the :datetime type above.