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:

Run in Livebook

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:

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, and ensure_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:

16 Likes

v1.2.4 – June 6, 2021

  • Speedup resolving the struct types
  • Limit the number of allowed fields types combinations to 4096
  • Support Range.t() and MapSet.t()
  • Keep type ensurers source code after compiling an umbrella project
  • Remove preconditions manifest file on mix clean command
  • List processed structs giving mix --verbose option
4 Likes

v1.2.5 – June 14, 2021

  • Add remote_types_as_any option to disable validation of specified remote types for the case of hitting the limit of 4096 type combinations. A precondition for wrapping user-defined type can be used in such cases.
1 Like

v1.2.6 - June 20, 2021

  • Validates type conformance of default values given with defstruct/1 at compile-time :star_struck:
  • Includes only the most matching type error into the error message
1 Like

v1.2.7 - July 4, 2021

  • Fix the bug to resolve remote type when giving correct alias
  • Support custom {:error, message} returned from the range validation function defined with precond/1 macro
2 Likes

v1.2.9 - August 8, 2021

Domo is lightened by extraction of the tagged_tuple library (finally! :tada:)

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/1 and required_fields/1 functions.

  • Add maybe_filter_precond_errors: true option that filters errors from precondition functions for better output for the user.

4 Likes

v1.3.0 - August 15, 2021

  • Change the default name of the constructor function to new! to follow Elixir naming convention.
    You can always change the name with the config :domo, :name_of_new_function, :new_func_name_here app configuration.

  • Fix bug to validate defaults for every required field in a struct except __underscored__ fields at compile-time.

  • Check whether the precondition function associated with t() type returns true at compile-time regarding defaults correctness check.

  • Add examples of integration with TypedStruct and TypedEctoSchema.

3 Likes

A post was merged into an existing topic: TaggedTuple

v1.3.2 - September 18, 2021

  • Support remote types in erlang modules like :inet.port_number()

  • Shorten the invalid value output in the error message

  • Increase validation speed by skipping fields that are not in t() type spec or have the any() type

  • Fix bug to skip validation of struct’s enforced keys default value because they are ignored during the construction anyway

  • Increase validation speed by generating TypeEnsurer modules for Date, Date.Range, DateTime, File.Stat, File.Stream, GenEvent.Stream, IO.Stream, Macro.Env, NaiveDateTime, Range, Regex, Task, Time, URI, and Version structs from the standard library at the first project compilation

  • Fix bug to call the precond function of the user type pointing to a struct

  • Increase validation speed by encouraging to use Domo or to make a precond function for struct referenced by a user type

  • Add Domo.has_type_ensurer?/1 that checks whether a TypeEnsurer module was generated for the given struct.

  • Add example of parsing and validating of the JSON via Jason + ExJSONPath + Domo

2 Likes

v1.3.3 - October 7, 2021

  • Support validation of Decimal.t()

  • Fix bug to define precondition function for user type referencing any() or term()

  • Add link to Commanded + Domo example app in Readme

1 Like

1.3.4 - October 13, 2021

  • Make error messages to be more informative

  • Improve compatibility with Ecto 3.7.x

  • Explicitly define :ecto and :decimal as optional dependencies

  • Fix bug to pass :remote_types_as_any option with use Domo

  • Explicitly define that MapSet should be validated with precond function for custom user type, because parametrized t(value) types are not supported

  • Replace apply() with Module.function calls to run faster

3 Likes

1.4.0 - November 15, 2021

  • Fix bug to detect runtime mode correctly when launched under test.

  • Add support for @opaque types.

Breaking changes:

  • Change new_ok constructor function name to new that is more convenient.
    Search and replace new_ok(new( in all files of the project
    using Domo to migrate.

  • Constructor function name generation procedure changes to adding !
    to the value of :name_of_new_function option. The defaults are new and new!.

1 Like

1.4.1 - November 16, 2021

  • Improve compatibility with Elixir v1.13

  • Format string representations of an anonymous function passed to precond/1 macro error message

2 Likes

1.5.0 - December 5, 2021

  • Fix bug to return explicit file read error message during the compile time

  • Completely replace apply() with . for validation function calls to run faster

  • Link planner server to mix process for better state handling

  • Support of the interactive use in iex and live book :star_struck:
    The code examples in Readme can be run interactively because it’s a livebook now!

Breaking change:

  • Improve compilation speed by starting resolve planner only once in Domo mix task.
    To migrate, please, put the :domo_compiler before :elixir in mix.exs.
    And do the same for reloadable_compilers key in config file
    if configured for Phoenix endpoint.

1.5.1 - December 12, 2021

  • Fix to detect mix compile with more reliable Code.can_await_module_compilation?

  • Fix to make benchmark run again as sub-project

  • Make :maybe_filter_precond_errors option to lift precondition error messages from the nested structs. That is for filtering error output down to a compact list of messages that can be presented to user :metal: See an example in the User facing error messages section of the Readme.

Run mix clean && mix compile to recompile the project in case of UndefinedFunctionError after the update.

2 Likes

1.5.2 - January 3, 2022

  • Support of structs referencing themselves to build trees like @type t :: %__MODULE__{left: t() | nil, right: t() | nil}.

  • Add project using Domo full recompilation time statistics. The compilation time slightly increases 14 → 15 seconds in a business application :sunglasses:

  • Fix the benchmark to make results deviation <12%. The creation of the struct with fields validation takes 3x times longer and 2x more memory when compared to creating the struct with possibly invalid data. What’s remarkable is that resource consumption grows linearly depending on the total number of fields in the nested struct :turtle:

This release finishes the roadmap drawn for the project. I’ve done everything planned, and I’m very proud of it!

The library will continue to evolve to be compatible with future Elixir releases and to address feedback from users :rocket:

2 Likes

The capabilities of this library look extremely similar to what Ecto’s Changeset provides.

Excluding the difference in the mechanism of how it’s used (precond and constructor functions instead of Changeset.cast and Changeset.validate_*), what is it that differentiates this from Changeset?

1 Like

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.

6 Likes

1.5.3 - April 11, 2022

  • Fix to generate type ensurers only in the scope of the given app in umbrella

  • Fix for Elixir v1.13 to recompile depending module’s type ensurer on the change of type in another module by deleting .beam file

  • Deprecate ensure_struct_defaults: false for skip_defaults: true, the former option is supported till the next version. Please, migrate to the latter one :relieved:

3 Likes

1.5.7 - August 6, 2022

  • Fix to resolve mfa() type correctly
  • Fix tests to acknowledge the random order of keys in map

Tested with elixir v1.14.0-rc.0 :metal:

4 Likes