Introduction
Exchema is a library to define and validate data. It allows you to check the type for a given value at runtime (it is not static type checking).
The type definition allows us to build other nice libraries on top of it like exchema_coercion
and exchema_stream_data
It uses the idea of refinement types, in which we have a global type (which all values belong) and can refine that type with the use of predicates.
The root type is :any
. From there we can declare subtypes with refinements
to reach the boundaries of possible values we want. The library comes with
several built-in type definitions for native types.
Let’s use the built-in types to show an example:
import Exchema.Notation # this is the entrypoint for the DSL
# Let's declare a type that is a subtype of String
subtype Name, Exchema.Types.String, []
# Now we have :any <- String <- Name
# Let's declare a structure using this type:
# here we use the structure macro which accepts an atom as the
# name for the structure. This will generate a struct so think of
# it as a different `defstruct` call
structure Names, [first: Name, last: Name]
# Pay attention at the 'default' values. They are type definitions.
# With that we can validate input.
# That was easy.
# Let's check if it is valid according to our refinements:
true = Exchema.is?(names, Names)
Nice. Although it does not look much, this is very powerful! Let’s see a more complex example.
Let’s define an Id
type. This will use the library UUID
from hex just
as an example.
subtype(
Id,
Exchema.Types.String,
fn val -> # this is our refinement
with {:ok, opts} <- UUID.info(val), # checks it is proper formed
4 <- opts[:version], # it is version 4
:default <- opts[:type] do # it is formatted as the default option
{:ok, val}
else
_ ->
{:error, :not_valid_uuid} # not valid UUID
end
end
)
Now if we want to declare a user we can do this:
structure User, [
id: Id,
first_name: Name,
last_name: Name
]
Awesome. Let’s again validate:
user = %User{id: nil, first_name: "Hello", last_name: "World"}
But… wait! id
nil is valid? Well, let’s validate to check:
Exchema.is?(user, User)
# false
We haven’t declared id
as optional. So, the structure is not valid.
Let’s fix our declaration and run again:
structure User, [
id: {Exchema.Types.Optional, Id},
first_name: Name,
last_name: Name
]
Wow. That is new. That is a parametric type (or parameterized type). It is
used for occasions like this one: the parameter can be either nil
or an
Id
.
It is also useful when you want to work with collection of elements like lists and maps. See the collections guide.
Let’s now validate again:
Exchema.is?(user, User)
# true
All right! That covers the basics.
See the Types guide for understanding the core concept behind refinement types.