Richer domain types with Ecto custom types
Notes
Primitive obsession makes our codebases harder to deal with.
Thankfully, Ecto custom types can help us transform primitives (or native types) into domain structures more quickly and at the boundary!
Suppose we have a custom %PhoneNumber{}
struct in our codebase with a module
that defines parse/1
and to_string/1
functions (to transform from and to
strings).
Now, we can define custom Ecto types like this:
defmodule Scout.Ecto.PhoneNumber do
use Ecto.Type
alias Scout.PhoneNumber
def type, do: :string
def cast(number) when is_binary(number) do
{:ok, PhoneNumber.parse(number)}
end
def cast(%PhoneNumber{} = number), do: {:ok, number}
def cast(_), do: :error
def load(number) when is_binary(number) do
{:ok, PhoneNumber.parse(number)}
end
def dump(%PhoneNumber{} = number), do: {:ok, PhoneNumber.to_string(number)}
def dump(_), do: :error
end
What does that module do?
- We
use Ecto.Type
to define the behavior and get some default functions, - We define a
cast/1
function to handle transforming user input (think of what happens withChangeset.cast
) into the richer data structure (in this case%PhoneNumber{}
, - We define a
load/1
function to transform data from the database into the richer data structure, and - We define a
dump/1
to transform the richer data structure into the representation we’ll insert into the database (string
in this case).
We can then update our schemas to use that custom type:
defmodule Scout.Accounts.Contact do
use Ecto.Schema
import Ecto.Changeset
schema "contacts" do
field :phone_number, Scout.Ecto.PhoneNumber
timestamps(type: :utc_datetime)
end
end
We can even use that custom type with embedded schemas (imagine one backing up a form):
defmodule ScoutWeb.NewAccount do
use Ecto.Schema
embedded_schema do
field :phone_number, Scout.Ecto.PhoneNumber
end
end
Without changing anything else, our domain can now deal exclusively with
%PhoneNumber{}
structs instead of strings.
- We transform user input into
%PhoneNumber{}
when we cast values in our changeset, and - We transform values from the database into our richer types when the data is pulled from the database.