Upgrade to Pro — share decks privately, control downloads, hide ads and more …

DOMO - Elixir library 
for business domain modelling with type-safe structs in micro-service setups

DOMO - Elixir library 
for business domain modelling with type-safe structs in micro-service setups

https://github.com/IvanRublev/Domo
https://hex.pm/packages/domo

## Variants of using Domo library ##

Compile-time (via `use Domo` in struct)

* Validate defaults given within `defstruct [filed: default_value]` to conform to the struct’s type t()
* Validate and maintain invariants for struct constants in @attributes, function calls, and other struct defaults that are built with `new/1`

Run-time (via calls to `new/1`, `ensure_type!/1`)

* Validate and maintain invariants for any struct value on build with `new/1` or after modification with `ensure_type/1`

Ivan Rublev

June 22, 2021
Tweet

More Decks by Ivan Rublev

Other Decks in Programming

Transcript

  1. DOMO - library 
 for business domain modelling with type-safe

    structs in micro-service setups Ivan Rublev
 HH.ex meetup, 22 Jun 2021 [email protected] @LevviBraun
  2. Struct definition challenge defmodule Customer do @enforce_keys [:id, :name, :delivery_address]

    defstruct @enforce_keys @type t ::: %___MODULE___{ id: String.t(), name: String.t(), delivery_address: :not_given | Address.t() } def new(fields \\ []), do: struct!(___MODULE___, fields) end defmodule Customer do use TypedStruct typedstruct do field :id, String.t(), enforce: true field :name, String.t(), enforce: true field :delivery_address, :not_given | Address.t() end def new(fields \\ []), do: struct!(___MODULE___, fields) end Hmm… types and constructor functions… 🤓
  3. Business Domain Modelling • AGGREGATE - group of nested structs

    representing a complicated real world object of in a given business process • From the “Blue Book” by Eric Evans • “Invariants, which are consistency rules that must be maintained whenever data changes, will involve relationships between members of the AGGREGATE” • Invariant example: sum of line prices <= approved limit * Excerpt From: Eric Evans. “Domain-Driven Design: Tackling Complexity in the Heart of Software.”
  4. Domo library for automatic type 
 conformance validation defmodule Customer

    do use Domo @enforce_keys [:id, :name, :delivery_address] defstruct @enforce_keys @type t ::: %___MODULE___{ id: String.t(), name: String.t(), delivery_address: :not_given | Address.t() } # def new(fields) # def new_ok(fields) # def ensure_type!(value) # def ensure_type_ok(value) end Customer.new(id: "55", name: "John") %Customer{id: "55", name: "John", delivery_address: :not_given} Customer.new(id: "55", name: nil, delivery_address: 3) *** (ArgumentError) the following values should have types defined for fields of the Customer struct: * Invalid value 3 for field :delivery_address of %Customer{}. Expected the value matching the :not_given | Address.t() type. * Invalid value nil for field :name of %Customer{}. Expected the value matching the <<<_:::_*8>>> type. customer_value ||> struct!(name: "John Bongiovi") ||> Customer.ensure_type!() %Customer{id: 55, name: "John Bongiovi", post_address: :not_given} user ||> struct!(name: :john_bongiovi) ||> Customer.ensure_type!() *** (ArgumentError) the following values should have types defined for fields of the Customer struct: * Invalid value :john_bongiovi for field :name of %Customer{}. 
 Expected the value matching the <<<_:::_*8>>> type. Validates field values with automatically generated pattern- matchings 🤩
  5. Domo - attach invariant to type with precondition macro defmodule

    PurchaseOrder do use Domo alias ___MODULE___.LineItem @enforce_keys [:id, :approved_limit] defstruct [:id, :approved_limit, line_items: []] @type t ::: %___MODULE___{ id: String.t(), approved_limit: integer(), line_items: [LineItem.t()] } end order = PurchaseOrder.new(id: "ORD-1245", approved_limit: 500)
 %PurchaseOrder{approved_limit: 500, id: "ORD-1245", line_items: []} line_item = PurchaseOrder.LineItem.new(part_name: "Gear", quantity: 3, price: 250, amount: 750)
 %PurchaseOrder.LineItem{amount: 750, part_name: "Gear", price: 250, quantity: 3} updated_order = %{order | line_items: [line_item]} ||> PurchaseOrder.ensure_type_ok()
 {:error, [t: "Invalid value %PurchaseOrder{approved_limit: 500, id: \"ORD-1245\", line_items:
 [%PurchaseOrder.LineItem{amount: 750, part_name: \"Gear\", price: 250, quantity: 3}]}. 
 Expected the value matching the PurchaseOrder.t() type. And a true value from the precondition 
 function \"&___MODULE___.approved_limit_invariant/1\" defined for PurchaseOrder.t() type.”]} defmodule PurchaseOrder.LineItem do use Domo @enforce_keys [:part_name, :quantity, :price, :amount] defstruct @enforce_keys @type t ::: %___MODULE___{ part_name: String.t(), quantity: integer(), price: integer(), amount: integer() } end precond t: &___MODULE___.approved_limit_invariant/1 def approved_limit_invariant(order) do order.line_items ||> Enum.map(& &1.amount) ||> Enum.sum() <<= order.approved_limit end
  6. Domo - validates struct defaults at compile-time In mix.exs: defp

    deps do [ {:domo, "~~> 1.2”}, … def project do [ elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix] +++ Mix.compilers() +++ [:domo_compiler], start_permanent: Mix.env() === :prod, … defmodule PurchaseOrder.LineItem do use Domo @enforce_keys [:part_name] defstruct [:part_name, amount: "one"] @type t ::: %___MODULE___{part_name: String.t(), amount: integer()} end ➜ example git:(master) mix compile === Compilation error in file lib/purchase_order/line_item.ex:1 === *** A default value given via defstruct/1 in PurchaseOrder.LineItem module mismatches the type. Invalid value "one" for field :amount of %PurchaseOrder.LineItem{}. Expected the value matching the integer() type.
  7. Module.TypeEnsurer Module.TypeEnsurer Struct Module Struct Module Domo compiler generates code

    to ensure type conformance from t() Struct Foo @type t :: %… new/1 esure_type!/1 Foo.TypeEnsurer ensure_field_type/1 Bar.TypeEnsurer ensure_field_type/1 :ok :error :ok :error Nested struct Bar Generated sources (manual change doesn’t affect compilation): project/_build/dev/domo_generated_code Pattern matching 🤩
  8. Library limitations for sum | types • Supports only 4096

    combinations of | typed fields for foreign structs not using Domo • How to address? • Take the ownership and put `use Domo` into the struct • Add heavy type to global `remote_types_as_any` option to disable generation of pattern matchings, wrap it into user-defined type with precondition to validate the value. t() ::: %DiskResource{ access: :read | :write | :read_write | :none, 4 type: :device | :directory | :regular | :other | :symlink, 5 atime: integer() | float() | :today | :month_ago | :year_ago, 5 ctime: integer() | float() | :today | :month_ago | :year_ago, 5 mtime: integer() | float() | :today | :month_ago | :year_ago, 5 mode: :read | :write | :append | :read_write | :read_append, 5 gid: non_neg_integer(), 1 size: non_neg_integer(), 1 uid: non_neg_integer() 1 } total combinations: 12_500 https://en.wikipedia.org/wiki/Rule_of_product - If there are a ways of doing something and b ways of doing another thing, then there are a · b ways of performing both actions.
  9. Variants of using Domo • Compile-time (via `use Domo` in

    struct) • Validate defaults given within `defstruct [filed: default_value]` to conform to the struct’s type t() • Validate and maintain invariants for struct constants in @attributes, function calls, and other struct defaults that are built with `new/1` • Run-time (via calls to `new/1`, `ensure_type!/1`) • Validate and maintain invariants for any struct value on build with `new/1` or after modification with `ensure_type/1`
  10. Domo • Download • {:domo, "~> 1.2”} • https://hex.pm/packages/domo •

    https://github.com/IvanRublev/Domo • Questions • https://elixirforum.com with tag #domo • elixir-lang.slack.com
 https://elixir-slackin.herokuapp.com/ channel #domo • Author - Ivan Rublev • [email protected] • Twitter: @LevviBraun It’d be cool if you can help to
 measure compile time without/with “use Domo” via
 mix deps.compile && mix clean && time mix compile and email result to me.