To enforce business rules, use the precond macro. Preconditions can be attached to specific fields or to the struct's type t() as a whole.
Field-level preconditions
Attach a precondition to a specific field type to ensure values meet certain criteria (e.g., range checks).
Type-level (invariant) preconditions
Attach a precondition to the t type to validate invariants involving multiple fields or nested structs.
Example:
defmodule PurchaseOrder do
use Domo
defstruct id: 1000, approved_limit: 200, items: []
@type id :: non_neg_integer()
precond(id: &(1000 <= &1 and &1 <= 5000))
@type t :: %__MODULE__{
id: id(),
approved_limit: pos_integer(),
items: [LineItem.t()]
}
precond(t: &validate_invariants/1)
defp validate_invariants(po) do
amounts = po.items |> Enum.map(& &1.amount) |> Enum.sum()
if amounts <= po.approved_limit do
:ok
else
{:error, "Sum of line item amounts (#{amounts}) should be <= to approved limit (#{po.approved_limit})."}
end
end
end
defmodule LineItem do
use Domo
defstruct amount: 0
@type t :: %__MODULE__{amount: non_neg_integer()}
end
defmodule PurchaseOrder do
use Domo
defstruct id: 1000, approved_limit: 200, items: []
@type id :: non_neg_integer()
precond(id: &(1000 <= &1 and &1 <= 5000))
@type t :: %__MODULE__{
id: id(),
approved_limit: pos_integer(),
items: [LineItem.t()]
}
precond(t: &validate_invariants/1)
defp validate_invariants(po) do
amounts = po.items |> Enum.map(& &1.amount) |> Enum.sum()
if amounts <= po.approved_limit do
:ok
else
{:error, "Sum of line item amounts (#{amounts}) should be <= to approved limit (#{po.approved_limit})."}
end
end
end