Examples

A bunch of Xema examples.

Basic

A minimal example.

iex> defmodule Example.Basic do
...>   use Xema, multi: true
...>
...>   xema :person do
...>     map(
...>       properties: %{
...>         first_name: :string,
...>         last_name: :string,
...>         age: {:integer, minimum: 0}
...>       }
...>     )
...>   end
...>
...>   @default true
...>   xema :foo, do: :string
...> end
iex>
iex> Example.Basic.valid?(
...>   :person,
...>   %{first_name: "James", last_name: "Brown", age: 42}
...> )
true
...> Example.Basic.valid?(
...>   :person,
...>   %{first_name: :james, last_name: "Brown", age: 42}
...> )
false
iex> {:error, error} = Example.Basic.validate(
...>   :person,
...>   %{first_name: :james, last_name: "Brown", age: 42}
...> )
{:error, %Xema.ValidationError{
  reason: %{properties: %{first_name: %{type: :string, value: :james}}}
}}
iex> Exception.message(error)
"Expected :string, got :james, at [:first_name]."
iex> Example.Basic.valid?("foo")
true
iex> Example.Basic.valid?(:foo)
false

Options

An example to check opts.

iex> defmodule Example.Options do
...>   use Xema
...>
...>   xema do
...>     keyword(
...>       properties: %{
...>         foo: atom(enum: [:bar, :baz]),
...>         limit: integer(minimum: 0),
...>         msg: :string
...>       },
...>       required: [:foo, :limit],
...>       additional_properties: false
...>     )
...>   end
...> end
iex>
iex> Example.Options.validate(foo: :bar, limit: 11, msg: "foo")
:ok
iex> {:error, error} = Example.Options.validate(foo: :foo, limit: 11)
{:error, %Xema.ValidationError{
  reason: %{properties: %{foo: %{enum: [:bar, :baz], value: :foo}}}
}}
iex> Exception.message(error)
"Value :foo is not defined in enum, at [:foo]."
iex> {:error, error} = Example.Options.validate(foo: :bar)
{:error, %Xema.ValidationError{
  reason: %{required: [:limit]}
}}
iex> Exception.message(error)
"Required properties are missing: [:limit]."
iex> {:error, error} = Example.Options.validate(foo: :bar, limit: 11, message: "foo")
{:error, %Xema.ValidationError{
  reason: %{properties: %{message: %{additional_properties: false}}}
}}
iex> Exception.message(error)
"Expected only defined properties, got key [:message]."

Custom validator

This example shows the use of an custom validator that is given as an tuple of module and function name.

iex> defmodule Example.Palindrome do
...>   def check(str) do
...>     case str == String.reverse(str) do
...>       true -> :ok
...>       false -> {:error, :no_palindrome}
...>     end
...>   end
...> end
iex>
iex> defmodule Example.PaliSchema do
...>   use Xema
...>
...>   xema :palindrome do
...>     string(validator: {Example.Palindrome, :check})
...>   end
...> end
...>
...> Example.PaliSchema.valid?(:palindrome, "racecar")
true
iex> Example.PaliSchema.valid?(:palindrome, "bike")
false
iex> {:error, error} = Example.PaliSchema.validate(:palindrome, "bike")
{:error, %Xema.ValidationError{
  reason: %{validator: :no_palindrome, value: "bike"}
}}
iex> Exception.message(error)
~s|Validator fails with :no_palindrome for value "bike".|

A validator can also be specified as behaviour.

iex> defmodule Example.PalindromeB do
...>   @behaviour Xema.Validator
...>
...>   @impl true
...>   def validate(str) do
...>     case str == String.reverse(str) do
...>       true -> :ok
...>       false -> {:error, :no_palindrome}
...>     end
...>   end
...> end
iex>
iex> defmodule Example.PaliSchemaB do
...>   use Xema
...>
...>   xema :palindrome do
...>     string(validator: Example.PalindromeB)
...>   end
...> end
...>
...> Example.PaliSchemaB.valid?(:palindrome, "racecar")
true
iex> Example.PaliSchemaB.valid?(:palindrome, "bike")
false
iex> {:error, error} = Example.PaliSchemaB.validate(:palindrome, "bike")
{:error, %Xema.ValidationError{
  reason: %{validator: :no_palindrome, value: "bike"}
}}
iex> Exception.message(error)
~s|Validator fails with :no_palindrome for value "bike".|

The custom validator can also be a part of the schema module.

iex> defmodule Example.Range do
...>   use Xema
...>
...>   xema :range do
...>     map(
...>       properties: %{
...>         from: integer(minimum: 0),
...>         to: integer(maximum: 100)
...>       },
...>       validator: &Example.Range.check/1
...>     )
...>   end
iex>
iex>   def check(%{from: from, to: to}) do
...>     case from < to do
...>       true -> :ok
...>       false -> {:error, :from_greater_to}
...>     end
...>   end
...> end
...>
...> Example.Range.validate(:range, %{from: 6, to: 8})
:ok
iex> {:error, error} = Example.Range.validate(:range, %{from: 66, to: 8})
{:error, %Xema.ValidationError{
  reason: %{validator: :from_greater_to, value: %{from: 66, to: 8}}
}}
iex> Exception.message(error)
"Validator fails with :from_greater_to for value %{from: 66, to: 8}."
iex> {:error, error} = Example.Range.validate(:range, %{from: 166, to: 118})
{:error, %Xema.ValidationError{
  reason: %{properties: %{to: %{maximum: 100, value: 118}}}
}}
iex> Exception.message(error)
"Value 118 exceeds maximum value of 100, at [:to]."

Cast JSON

The following example cast data structure that is decoded by Jason.decode!/1. For the encoding of URI a Jason.Encoder implementation is needed.

defimpl Jason.Encoder, for: URI do
  def encode(uri, _opts) do
    ~s|"#{URI.to_string(uri)}"|
  end
end

This example shows how a complex data structure decoded by a JSON parser can be converted in a form described by a schema.

iex> defmodule CasterUri do
...>   @behaviour Xema.Caster
...>
...>   @impl true
...>   def cast(%URI{} = uri), do: {:ok, uri}
...>
...>   def cast(string) when is_binary(string), do: {:ok, URI.parse(string)}
...>
...>   def cast(_), do: :error
...> end
iex>
iex> defmodule UserSchema do
...>   use Xema
...>
...>   xema :user do
...>     map(
...>       keys: :atoms,
...>       properties: %{
...>         name: :string,
...>         birthday: strux(Date),
...>         favorites:
...>           map(
...>             keys: :atoms,
...>             properties: %{
...>               fruits: list(items: atom(enum: [:apple, :orange, :banana])),
...>               uris: list(items: strux(URI, caster: CasterUri))
...>             }
...>           )
...>       },
...>       additional_properties: false
...>     )
...>   end
...> end
iex>
iex> {:ok, json} =
...>   %{
...>     "name" => "Nick",
...>     "birthday" => ~D|2000-04-17|,
...>     "favorites" => %{
...>       "fruits" => ~w(apple banana),
...>       "uris" => ["https://elixir-lang.org/"]
...>     }
...>   }
...>   |> UserSchema.cast!()
...>   |> Jason.encode()
{:ok, "{\"birthday\":\"2000-04-17\",\"favorites\":{\"fruits\":[\"apple\",\"banana\"],\"uris\":[\"https://elixir-lang.org/\"]},\"name\":\"Nick\"}"}
iex>
iex> json |> Jason.decode!() |> UserSchema.cast!()
%{
  birthday: ~D[2000-04-17],
  favorites: %{
    fruits: [:apple, :banana],
    uris: [
      %URI{
        authority: "elixir-lang.org",
        fragment: nil,
        host: "elixir-lang.org",
        path: "/",
        port: 443,
        query: nil,
        scheme: "https",
        userinfo: nil
      }
    ]
  },
  name: "Nick"
}

Struct

This example combines some schemas in a schema for a struct.

The first schema describes a key-value map with a string key and a value of type number or string.

defmodule ExApp.KeyValue do
  use Xema

  xema do
    map(
      keys: :strings,
      additional_properties: [:number, :string],
      property_names: [pattern: ~r/^[a-z][a-z_]*$/],
      default: %{}
    )
  end
end

This schema is used as follows:

assert KeyValue.valid?(%{"str" => "Foo", "num" => 5})
assert KeyValue.cast(str: "Foo", num: 5) == {:ok, %{"str" => "Foo", "num" => 5}}

The next schema is a simple struct schema.

defmodule ExApp.Location do
  use Xema

  xema do
    field :city, [:string, nil]
    field :country, [:string, nil], min_length: 1
  end
end

With a cast, a Location struct is returned.

assert Location.cast(city: "Berlin") == {:ok, %Location{city: "Berlin", country: nil}}

The Grant schema comes with two required fields.

defmodule ExApp.Grant do
  use Xema

  @ops [:foo, :bar, :baz]
  @permissions [:create, :read, :update, :delete]

  xema do
    field :op, :atom, enum: @ops
    field :permissions, :list, items: {:atom, enum: @permissions}
    required [:op, :permissions]
  end
end

The example contains also a Caster for unix timestamps.

defmodule ExApp.UnixTimestamp do
  @behaviour Xema.Caster

  @impl true
  def cast(timestamp) when is_integer(timestamp), do: {:ok, DateTime.from_unix!(timestamp)}

  def cast(%DateTime{} = timestamp), do: timestamp

  def cast(_), do: :error
end

All schemas above are use in the User schema.

defmodule ExApp.User do
  use Xema

  alias ExApp.{Grant, KeyValue, Location, UnixTimestamp}

  @regex_uuid ~r/^[a-z0-9]{8}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{12}$/

  xema do
    field :id, :string, default: {UUID, :uuid4}, pattern: @regex_uuid
    field :name, :string, min_length: 1
    field :age, [:integer, nil], minimum: 0
    field :location, Location
    field :grants, :list, items: Grant, default: []
    field :settings, KeyValue
    field :created, DateTime, caster: UnixTimestamp
    field :updated, DateTime, caster: UnixTimestamp, allow: nil
    required [:age]
  end
end

This module used also UUID to set a default for the field id.

A call of User.cast!

ExApp.User.cast!(
  name: "Nick",
  age: 21,
  location: [city: "Dortmud", country: "Germany"],
  grants: [%{op: :bar, permissions: [:read, :update]}],
  settings: [foo: 44, bar: "baz"],
  created: 1_567_922_779
)

returns a User struct

%ExApp.User{
  age: 21,
  created: ~U[2019-09-08 06:06:19Z],
  grants: [%ExApp.Grant{op: :bar, permissions: [:read, :update]}],
  id: "c5166552-25f5-43fe-91de-969344fd67d6",
  location: %ExApp.Location{city: "Dortmud", country: "Germany"},
  name: "Nick",
  settings: %{"bar" => "baz", "foo" => 44},
  updated: nil
}

Validate with option :fail

With the option :fail, you can define when the validation is aborted. This also influences how many error reasons are returned.

  • :immediately aborts the validation when the first validation fails.
  • :early (default) aborts on failed validations, but runs validations for all properties and items.
  • :finally aborts after all possible validations.

Examples

iex> schema = Xema.new({:list, max_items: 3, items: :integer})
iex> data = [1, "a", "b"]
iex> {:error, error} = Xema.validate(schema, data, fail: :immediately)
iex> error.reason
%{items: %{
  1 => %{type: :integer, value: "a"}
}}
iex> {:error, error} = Xema.validate(schema, data, fail: :early)
iex> error.reason
%{items: %{
  1 => %{type: :integer, value: "a"},
  2 => %{type: :integer, value: "b"}
}}
iex> {:error, error} = Xema.validate(schema, data, fail: :finally)
iex> error.reason
%{items: %{
  1 => %{type: :integer, value: "a"},
  2 => %{type: :integer, value: "b"}
}}
iex> # new data
iex> data = [1, "a", "b", 4]
iex> {:error, error} = Xema.validate(schema, data, fail: :immediately)
iex> error.reason
%{max_items: 3, value: [1, "a", "b", 4]}
iex> {:error, error} = Xema.validate(schema, data, fail: :early)
iex> error.reason
%{max_items: 3, value: [1, "a", "b", 4]}
iex> {:error, error} = Xema.validate(schema, data, fail: :finally)
iex> error.reason
[
  %{items: %{
    1 => %{type: :integer, value: "a"},
    2 => %{type: :integer, value: "b"}
  }},
  %{max_items: 3, value: [1, "a", "b", 4]}
]