StructCop is a library that was aimed to introduce data correctness and type coercion into Elixir structs. It also simplifies building valid structs with smart constructors.
Add struct_cop
to your list of dependencies in mix.exs
:
def deps do
[
{:struct_cop, "~> 0.2.0"}
]
end
Define a contract for given entity. We will be describing a single measure for sport activity:
defmodule Measurement do
use StructCop
contract do
field :elevation, :float
field :lat, :float
field :lng, :float
field :tracked_at, :utc_datetime_usec
end
def validate(changeset) do
import Ecto.Changeset
changeset
|> validate_required([:tracked_at, :lat, :lng])
|> validate_number(:lat, greater_than_or_equal_to: -90, less_than_or_equal_to: 90)
|> validate_number(:lng, greater_than_or_equal_to: -180, less_than_or_equal_to: 180)
end
end
From that moment we can:
> Measurement.new!(lat: 10, lng: 20, tracked_at: DateTime.utc_now())
%Measurement{
elevation: nil,
lat: 10.0,
lng: 20.0,
tracked_at: ~U[2019-12-28 18:36:01.625656Z]
}
It also casts fields from binaries:
> Measurement.new!(lat: "52.409538", lng: "16.931992", tracked_at: "2019-12-28T18:30:43.288080+00:00")
%Measurement{
elevation: nil,
lat: 52.409538,
lng: 16.931992,
tracked_at: ~U[2019-12-28 18:30:43.288080Z]
}
Passing invalid attributes raises an ArgumentError
:
> Measurement.new!(lat: "not_a_float", lng: 200.10)
** (ArgumentError) cast failed for %Measurement{elevation: nil, lat: nil, lng: nil, tracked_at: nil}:
params:
%{"lat" => "not_a_float", "lng" => 200.1}
errors:
[lng: {"must be less than or equal to %{number}", [validation: :number, kind: :less_than_or_equal_to, number: 180]}, tracked_at: {"can't be blank", [validation: :required]}, lat: {"is invalid", [type: :float, validation: :cast]}]
(struct_cop) lib/struct_cop.ex:47: StructCop.cast!/2
- Tobiasz Małecki - discussing initial idea
- Maciej Kaszubowski - discussing initial idea