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"
}