IvanR
Domo - model a business domain with type-safe structs and field type range checks
Domo makes your structure types work for data conformance validation. And it enables automatic range checking for field types at run-time.
Domo adds a new/1 constructor function to the structure module that builds an instance only if all fields conform to the struct’s t() type and all specified precondition functions for filed types return true.
That is useful for boundary data validation coming as decoded JSON from Jason or from :erlang.binary_to_term/1, and for validation when converting one core struct into another like in CQRS framework Commanded.
Domo makes it possible to validate model types in microservice setups between depending applications by sharing common modules with type definitions among codebases.
See typical usage in Readme.md via:
Shortly it looks like:
defmodule Customer do
use Domo
defstruct title: :none, name: "", age: 0
@type title :: :mr | :ms | :dr | :none
@type name :: String.t()
@type age :: non_neg_integer()
precond age: &(&1 < 300)
@type t :: %__MODULE__{title: title(), name: name(), age: age()}
precond t: &(String.length(&1.name) < 10)
end
iex(1)> Customer.new(title: :dr, name: "John", age: 25)
{:ok, %Customer{age: 25, name: "John", title: :dr}}
iex(2)> Customer.new(title: :dr, name: nil, age: 25)
{:error, [name: "Invalid value nil for field :name of %Customer{}. Expected the value \
matching the <<_::_*8>> type."]}
iex(3)> Customer.new(title: :dr, name: "Johnny be good", age: 25)
{:error, [t: "Invalid value %Customer{age: 25, name: \"Johnny be good\", title: :dr}. \
Expected the value matching the Customer.t() type. And a true value from the precondition \
function \"&(String.length(&1.name) < 10)\" defined for Customer.t() type."]}
Example applications:
- Web-site with Ecto + Domo
- Integration with
TypedStructandTypedEctoSchemalibraries - JSON parsing and validation
- Commanded + Domo combo used in Event Sourcing and CQRS
Domo main features are:
- automatic generation of constructor and insurance functions validating struct’s fields conformance to
@type t(), which are:new!/1,new/1,ensure_type!/1, andensure_type/1 - validation of nested structs referenced in the type spec
- support for boolean precondition functions attached to the user-defined type or the whole struct’s
t()type for values range check - validation of struct default values at compile-time
- recompilation of dependencies when type definition changes
More information here:
Most Liked
IvanR
The precond, which works only in association with the given type, is more like an extension to constrain the type’s values even more or to provide a custom error message.
The main difference in approaches is that Domo focuses on declarative constraint definition for structs with types combinations. And that the validation of nested structs is supported automatically just by @type spec. You can find an example illustrating that in the readme.
With Domo, it takes less code to have validation functions for structs. That can take less time to change them later.
IvanR
1.5.8 - September 11, 2022
- Domo generated constructor functions
new!/1/0andnew/2/1/0become overridable. - Improves Ecto changeset validation. Now the
Domo.Changeset.validate_type/1skipshas_manyand other assoc fields automatically, so thecast_assoc(:field)can be called to do the associations validation sequentually.
The one validate_type/1 call in the changeset gives a 1.5x slower execution than the bunch of equivalent Ecto’s validate_... calls.
At the same time, the execution duration of the pure Domo generated constructor function new!/1 is the same as of building a struct with Ecto changeset approach.
Comparison:
Domo Album.new!/1 5.31
Ecto.Changeset validate_.../1 5.21 - 1.02x slower +3.59 ms
Domo.Changeset validate_type/1 3.76 - 1.41x slower +77.93 ms
You can find all benchmark results in the README ![]()
The overridable constructors open the remarkable feature of injecting the generated default values into a struct, keeping the values validated. Let the struct using Domo have a token field, then the generation of the default value can be done like that:
def new!([_|_] = fields) do
super(Keyword.merge(fields, token: random_string(8)))
end
So no matter what random_string returns, the super function will ensure that it matches the type of the token field defined in t().
Magnificent! ![]()
IvanR
IvanR
v1.2.4 – June 6, 2021
- Speedup resolving the struct types
- Limit the number of allowed fields types combinations to 4096
- Support
Range.t()andMapSet.t() - Keep type ensurers source code after compiling an umbrella project
- Remove preconditions manifest file on
mix cleancommand - List processed structs giving mix
--verboseoption
IvanR
v1.2.9 - August 8, 2021
Domo is lightened by extraction of the tagged_tuple library (finally!
)
Documentation is rewritten from scratch.
The /example_avialia project is updated to demonstrate using of Domo to validate Ecto changesets.
Other changes:
-
Fix bug to acknowledge that type has been changed after a failed compilation.
-
Fix bug to match structs not using Domo with a field of
any()type with and without precondition. -
Add
typed_fields/1andrequired_fields/1functions. -
Add
maybe_filter_precond_errors: trueoption that filters errors from precondition functions for better output for the user.









