Current section
43 Versions
Jump to
Current section
43 Versions
Compare versions
17
files changed
+1087
additions
-561
deletions
| @@ -1,22 +1,19 @@ | |
| 1 1 | # Domo |
| 2 2 | |
| 3 | - [](https://travis-ci.com/IvanRublev/domo) |
| 4 | - [](#domo) |
| 5 | - [](https://hex.pm/packages/domo) |
| 3 | + ## Section |
| 6 4 | |
| 7 | - :warning: This library generates code for structures that can bring suboptimal compilation times increased to approx 20% |
| 5 | + |[](https://travis-ci.com/IvanRublev/domo)|[](#domo)|[](https://hex.pm/packages/domo)| |
| 6 | + |-|-|-| |
| 8 7 | |
| 9 | - :information_source: The usage example is in [/example_avialia](/example_avialia) directory. |
| 8 | + ⚠️ This library generates code for structures that can bring suboptimal compilation times increased to approximately 20% |
| 10 9 | |
| 11 | - :information_source: Examples of integration with `TypedStruct` and `TypedEctoSchema` are in [/example_typed_integrations](/example_typed_integrations) directory. |
| 10 | + đź”— JSON parsing and validation example is in https://github.com/IvanRublev/contentful-elixir-parse-example-nestru-domo repo. |
| 12 11 | |
| 13 | - :information_source: JSON parsing and validation example is in [contentful-elixir-parse-example-nestru-domo](https://github.com/IvanRublev/contentful-elixir-parse-example-nestru-domo) repo. |
| 14 | - |
| 15 | - :information_source: Commanded + Domo combo used in Event Sourcing and CQRS example app is in https://github.com/IvanRublev/bank-commanded-domo repo. |
| 12 | + đź”— Commanded + Domo combo used in Event Sourcing and CQRS example app is in https://github.com/IvanRublev/bank-commanded-domo repo. |
| 16 13 | |
| 17 14 | --- |
| 18 15 | |
| 19 | - [//]: # (Documentation) |
| 16 | + <!-- Documentation --> |
| 20 17 | |
| 21 18 | A library to ensure the consistency of structs modelling a business domain via |
| 22 19 | their `t()` types and associated precondition functions. |
| @@ -25,9 +22,9 @@ Used in a struct's module, the library adds constructor, validation, | |
| 25 22 | and reflection functions. Constructor and validation functions |
| 26 23 | guarantee the following at call time: |
| 27 24 | |
| 28 | - * A complex struct conforms to its `t()` type. |
| 29 | - * Structs are validated to be consistent to follow given business rules by |
| 30 | - precondition functions associated with struct types. |
| 25 | + * A complex struct conforms to its `t()` type. |
| 26 | + * Structs are validated to be consistent to follow given business rules by |
| 27 | + precondition functions associated with struct types. |
| 31 28 | |
| 32 29 | If the conditions described above are not met, the constructor |
| 33 30 | and validation functions return an error. |
| @@ -38,92 +35,197 @@ across all structs referencing the type. | |
| 38 35 | In terms of Domain Driven Design the invariants relating structs to each other |
| 39 36 | can be defined with types and associated precondition functions. |
| 40 37 | |
| 41 | - Let's say that we have a `PurchaseOrder` and `LineItem` structs with relating |
| 38 | + --- |
| 39 | + |
| 40 | + <p align="center"> |
| 41 | + <a href="https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2FIvanRublev%2FDomo%2Fblob%2Fmaster%2FREADME.md"> |
| 42 | + <img src="https://livebook.dev/badge/v1/blue.svg" alt="Run in Livebook" /> |
| 43 | + </a> |
| 44 | + </p> |
| 45 | + |
| 46 | + ```elixir |
| 47 | + Mix.install([:domo], force: true) |
| 48 | + ``` |
| 49 | + |
| 50 | + --- |
| 51 | + |
| 52 | + Let's say that we have a `LineItem` and `PurchaseOrder` structs with relating |
| 42 53 | invariant that is the sum of line item amounts should be less then order's |
| 43 54 | approved limit. That can be expressed like the following: |
| 44 55 | |
| 45 56 | ```elixir |
| 46 | - defmodule PurchaseOrder do |
| 47 | - use Domo |
| 48 | - |
| 49 | - defstruct [id: 1000, approved_limit: 200, items: []] |
| 50 | - |
| 51 | - @type id :: non_neg_integer() |
| 52 | - precond id: &(1000 <= &1 and &1 <= 5000) |
| 53 | - |
| 54 | - @type t :: %__MODULE__{ |
| 55 | - id: id(), |
| 56 | - approved_limit: pos_integer(), |
| 57 | - items: [LineItem.t()] |
| 58 | - } |
| 59 | - precond t: &validate_invariants/1 |
| 60 | - |
| 61 | - defp validate_invariants(po) do |
| 62 | - cond do |
| 63 | - po.items |> Enum.map(& &1.amount) |> Enum.sum() > po.approved_limit -> |
| 64 | - {:error, "Sum of line item amounts should be <= to approved limit"} |
| 65 | - |
| 66 | - true -> |
| 67 | - :ok |
| 68 | - end |
| 69 | - end |
| 70 | - end |
| 71 | - |
| 72 57 | defmodule LineItem do |
| 73 58 | use Domo |
| 74 59 | |
| 75 | - defstruct [amount: 0] |
| 60 | + defstruct amount: 0 |
| 76 61 | |
| 77 62 | @type t :: %__MODULE__{amount: non_neg_integer()} |
| 78 63 | end |
| 64 | + |
| 65 | + defmodule PurchaseOrder do |
| 66 | + use Domo |
| 67 | + |
| 68 | + defstruct id: 1000, approved_limit: 200, items: [] |
| 69 | + |
| 70 | + @type id :: non_neg_integer() |
| 71 | + precond(id: &(1000 <= &1 and &1 <= 5000)) |
| 72 | + |
| 73 | + @type t :: %__MODULE__{ |
| 74 | + id: id(), |
| 75 | + approved_limit: pos_integer(), |
| 76 | + items: [LineItem.t()] |
| 77 | + } |
| 78 | + precond(t: &validate_invariants/1) |
| 79 | + |
| 80 | + defp validate_invariants(po) do |
| 81 | + amounts = po.items |> Enum.map(& &1.amount) |> Enum.sum() |
| 82 | + |
| 83 | + if amounts <= po.approved_limit do |
| 84 | + :ok |
| 85 | + else |
| 86 | + {:error, "Sum of line item amounts (#{amounts}) should be <= to approved limit (#{po.approved_limit})."} |
| 87 | + end |
| 88 | + end |
| 89 | + end |
| 79 90 | ``` |
| 80 91 | |
| 81 | - Then `PurchaseOrder` struct can be constructed consistently like that: |
| 92 | + Then `PurchaseOrder` struct can be constructed consistently with functions generated by Domo like the following: |
| 82 93 | |
| 83 94 | ```elixir |
| 84 | - iex> {:ok, po} = PurchaseOrder.new() |
| 95 | + PurchaseOrder.new() |
| 96 | + ``` |
| 97 | + ```output |
| 85 98 | {:ok, %PurchaseOrder{approved_limit: 200, id: 1000, items: []}} |
| 99 | + ``` |
| 86 100 | |
| 87 | - iex> PurchaseOrder.new(id: 500, approved_limit: 0) |
| 101 | + The constructor function takes any `Enumerable` as the input value: |
| 102 | + |
| 103 | + ```elixir |
| 104 | + {:ok, po} = PurchaseOrder.new(%{approved_limit: 250}) |
| 105 | + ``` |
| 106 | + ```output |
| 107 | + {:ok, %PurchaseOrder{approved_limit: 250, id: 1000, items: []}} |
| 108 | + ``` |
| 109 | + |
| 110 | + It returns the descriptive keyword list if there is an error in input arguments. |
| 111 | + And it validates nested structs automatically: |
| 112 | + |
| 113 | + ```elixir |
| 114 | + PurchaseOrder.new(id: 500, items: [%LineItem{amount: -5}]) |
| 115 | + ``` |
| 116 | + ```output |
| 88 117 | {:error, |
| 89 | - [ |
| 90 | - id: "Invalid value 500 for field :id of %PurchaseOrder{}. Expected the |
| 118 | + [ |
| 119 | + items: "Invalid value [%LineItem{amount: -5}] for field :items of %PurchaseOrder{}. |
| 120 | + Expected the value matching the [%LineItem{}] type. |
| 121 | + Underlying errors: |
| 122 | + - The element at index 0 has value %LineItem{amount: -5} that is invalid. |
| 123 | + - Value of field :amount is invalid due to Invalid value -5 for field :amount |
| 124 | + of %LineItem{}. Expected the value matching the non_neg_integer() type.", |
| 125 | + id: "Invalid value 500 for field :id of %PurchaseOrder{}. Expected the |
| 91 126 | value matching the non_neg_integer() type. And a true value from |
| 92 127 | the precondition function \"&(1000 <= &1 and &1 <= 5000)\" |
| 93 | - defined for PurchaseOrder.id() type.", |
| 94 | - approved_limit: "Invalid value 0 for field :approved_limit of %PurchaseOrder{}. |
| 95 | - Expected the value matching the pos_integer() type." |
| 96 | - ]} |
| 128 | + defined for PurchaseOrder.id() type." |
| 129 | + ]} |
| 130 | + ``` |
| 97 131 | |
| 98 | - iex> updated_po = %{po | items: [LineItem.new!(amount: 150), LineItem.new!(amount: 100)]} |
| 99 | - %PurchaseOrder{ |
| 100 | - approved_limit: 200, |
| 101 | - id: 1000, |
| 102 | - items: [%LineItem{amount: 150}, %LineItem{amount: 100}] |
| 103 | - } |
| 132 | + And manually updated struct can be validated like the following: |
| 104 133 | |
| 105 | - iex> PurchaseOrder.ensure_type(updated_po) |
| 106 | - {:error, [t: "Sum of line item amounts should be <= to approved limit"]} |
| 107 | - |
| 108 | - iex> updated_po = %{po | items: [LineItem.new!(amount: 150)]} |
| 109 | - %PurchaseOrder{approved_limit: 200, id: 1000, items: [%LineItem{amount: 150}]} |
| 110 | - |
| 111 | - iex> PurchaseOrder.ensure_type(updated_po) |
| 134 | + ```elixir |
| 135 | + po |
| 136 | + |> Map.put(:items, [LineItem.new!(amount: 150)]) |
| 137 | + |> PurchaseOrder.ensure_type() |
| 138 | + ``` |
| 139 | + ```output |
| 112 140 | {:ok, %PurchaseOrder{approved_limit: 200, id: 1000, items: [%LineItem{amount: 150}]}} |
| 113 141 | ``` |
| 114 142 | |
| 143 | + Domo returns the error if the precondition function attached to the `t()` type |
| 144 | + that validates invariants for the struct as a whole fails: |
| 145 | + |
| 146 | + ```elixir |
| 147 | + updated_po = %{po | items: [LineItem.new!(amount: 180), LineItem.new!(amount: 100)]} |
| 148 | + PurchaseOrder.ensure_type(updated_po) |
| 149 | + ``` |
| 150 | + ```output |
| 151 | + {:error, [t: "Sum of line item amounts should be <= to approved limit"]} |
| 152 | + ``` |
| 153 | + |
| 154 | + Getting the list of the required fields of the struct that have type other |
| 155 | + then `nil` or `any` is like that: |
| 156 | + |
| 157 | + ```elixir |
| 158 | + PurchaseOrder.required_fields() |
| 159 | + ``` |
| 160 | + ```output |
| 161 | + [:approved_limit, :id, :items] |
| 162 | + ``` |
| 163 | + |
| 115 164 | See the [Callbacks](#callbacks) section for more details about functions added to the struct. |
| 116 165 | |
| 166 | + ## Integration with Ecto |
| 167 | + |
| 168 | + Ecto schema changeset can be automatically validated to conform to `t()` type |
| 169 | + and associated preconditions. Then the changeset function can be like the following: |
| 170 | + |
| 171 | + <!-- livebook:{"force_markdown":true} --> |
| 172 | + |
| 173 | + ```elixir |
| 174 | + defmodule Customer do |
| 175 | + use Ecto.Schema |
| 176 | + use Domo, ensure_struct_defaults: false |
| 177 | + |
| 178 | + import Ecto.Changeset |
| 179 | + import Domo.Changeset |
| 180 | + |
| 181 | + schema "customers" do |
| 182 | + field :first_name, :string |
| 183 | + field :last_name, :string |
| 184 | + field :birth_date, :date |
| 185 | + |
| 186 | + timestamps() |
| 187 | + end |
| 188 | + |
| 189 | + @type t :: %__MODULE__{ |
| 190 | + first_name: String.t(), |
| 191 | + last_name: String.t(), |
| 192 | + birth_date: Date.t() |
| 193 | + } |
| 194 | + |
| 195 | + def changeset(changeset, attrs) do |
| 196 | + changeset |
| 197 | + |> cast(attrs, typed_fields()) |
| 198 | + |> validate_required(required_fields()) |
| 199 | + |> validate_type() |
| 200 | + end |
| 201 | + |
| 202 | + # Domo adds typed_fields/0, required_fields/0 funcitons to the schema. |
| 203 | + # Domo.Cahngeset defines validate_type/1 function. |
| 204 | + end |
| 205 | + ``` |
| 206 | + |
| 207 | + See `typed_fields/0`, `required_fields/0`, and `Domo.Changeset` module |
| 208 | + documentation for details. |
| 209 | + |
| 210 | + See detailed example is in the [./example_avialia](https://github.com/IvanRublev/Domo/tree/master/example_avialia) project. |
| 211 | + |
| 212 | + ## Integration with libraries generating t() type for a struct |
| 213 | + |
| 214 | + Domo is compatible with most libraries that generate `t()` type for a struct |
| 215 | + or an Ecto schema. Just `use Domo` in the module, and that's it. |
| 216 | + |
| 217 | + An advanced example is in the [./example_typed_integrations](https://github.com/IvanRublev/Domo/tree/master/example_typed_integrations) project. |
| 218 | + |
| 117 219 | ## Compile-time and Run-time validations |
| 118 220 | |
| 119 221 | At the project's compile-time, Domo can perform the following checks: |
| 120 222 | |
| 121 | - * It automatically validates that the default values given with `defstruct/1` |
| 122 | - conform to struct's type and fulfill preconditions. |
| 223 | + * It automatically validates that the default values given with `defstruct/1` |
| 224 | + conform to struct's type and fulfill preconditions. |
| 123 225 | |
| 124 | - * It ensures that the struct using Domo built with `new!/1` function |
| 125 | - to be a function's default argument or a struct field's default value |
| 126 | - matches its type and preconditions. |
| 226 | + * It ensures that the struct using Domo built with `new!/1` function |
| 227 | + to be a function's default argument or a struct field's default value |
| 228 | + matches its type and preconditions. |
| 127 229 | |
| 128 230 | Domo validates struct type conformance with appropriate `TypeEnsurer` modules |
| 129 231 | built during the project's compilation at the application's run-time. |
| @@ -144,12 +246,16 @@ between module defining the struct's field and module defining the field's final | |
| 144 246 | |
| 145 247 | To use Domo in a project, add the following line to `mix.exs` dependencies: |
| 146 248 | |
| 249 | + <!-- livebook:{"force_markdown":true} --> |
| 250 | + |
| 147 251 | ```elixir |
| 148 252 | {:domo, "~> 1.2.0"} |
| 149 253 | ``` |
| 150 254 | |
| 151 255 | And the following line to the compilers: |
| 152 256 | |
| 257 | + <!-- livebook:{"force_markdown":true} --> |
| 258 | + |
| 153 259 | ```elixir |
| 154 260 | compilers: Mix.compilers() ++ [:domo_compiler] |
| 155 261 | ``` |
| @@ -157,51 +263,35 @@ compilers: Mix.compilers() ++ [:domo_compiler] | |
| 157 263 | To avoid `mix format` putting extra parentheses around `precond/1` macro call, |
| 158 264 | add the following import to the `.formatter.exs`: |
| 159 265 | |
| 266 | + <!-- livebook:{"force_markdown":true} --> |
| 267 | + |
| 160 268 | ```elixir |
| 161 269 | [ |
| 162 270 | import_deps: [:domo] |
| 163 271 | ] |
| 164 272 | ``` |
| 165 273 | |
| 166 | - ## Usage with Phoenix hot reload |
| 274 | + ## Setup for Phoenix hot reload |
| 167 275 | |
| 168 | - To call functions added by Domo from a Phoenix controller, add the following |
| 276 | + To enable hot reload for type changes in structs using Domo, add the following |
| 169 277 | line to the endpoint's configuration in the `config.exs` file: |
| 170 278 | |
| 279 | + <!-- livebook:{"force_markdown":true} --> |
| 280 | + |
| 171 281 | ```elixir |
| 172 282 | config :my_app, MyApp.Endpoint, |
| 173 283 | reloadable_compilers: [:phoenix] ++ Mix.compilers() ++ [:domo_compiler] |
| 174 284 | ``` |
| 175 285 | |
| 176 | - Otherwise, type changes wouldn't be hot-reloaded by Phoenix. |
| 286 | + <!-- Documentation --> |
| 177 287 | |
| 178 | - ## Usage with Ecto |
| 288 | + ## Callbacks |
| 179 289 | |
| 180 | - Ecto schema changeset can be automatically validated to conform to `t()` type |
| 181 | - and fulfil associated preconditions. |
| 182 | - |
| 183 | - See `Domo.Changeset` module documentation for details. |
| 184 | - |
| 185 | - See the example app using Domo to validate Ecto changesets |
| 186 | - in the `/example_avialia` folder of this repository. |
| 187 | - |
| 188 | - ## Usage with libraries generating t() type for a struct |
| 189 | - |
| 190 | - Domo is compatible with most libraries that generate `t()` type for a struct |
| 191 | - or an Ecto schema. Just `use Domo` in the module, and that's it. |
| 192 | - |
| 193 | - An advanced example is in the `/example_typed_integrations` folder |
| 194 | - of this repository. |
| 195 | - |
| 196 | - [//]: # (Documentation) |
| 197 | - |
| 198 | - ## <a name="callbacks"></a>Constructor, validation, and reflection functions added to the current module |
| 290 | + Constructor, validation, and reflection functions added to the struct module using Domo. |
| 199 291 | |
| 200 292 | ### new!/1/0 |
| 201 293 | |
| 202 | - <blockquote> |
| 203 | - |
| 204 | - [//]: # (new!/1) |
| 294 | + <!-- new!/1 --> |
| 205 295 | |
| 206 296 | Creates a struct validating type conformance and preconditions. |
| 207 297 | |
| @@ -217,15 +307,11 @@ Raises an `ArgumentError` if conditions described above are not fulfilled. | |
| 217 307 | This function will check if every given key-value belongs to the struct |
| 218 308 | and raise `KeyError` otherwise. |
| 219 309 | |
| 220 | - [//]: # (new!/1) |
| 221 | - |
| 222 | - </blockquote> |
| 310 | + <!-- new!/1 --> |
| 223 311 | |
| 224 312 | ### new/2/1/0 |
| 225 313 | |
| 226 | - <blockquote> |
| 227 | - |
| 228 | - [//]: # (new/2) |
| 314 | + <!-- new/2 --> |
| 229 315 | |
| 230 316 | Creates a struct validating type conformance and preconditions. |
| 231 317 | |
| @@ -245,7 +331,7 @@ the field and value is the string with the error message. | |
| 245 331 | Keys in the `enumerable` that don't exist in the struct |
| 246 332 | are automatically discarded. |
| 247 333 | |
| 248 | - ## Options |
| 334 | + #### Options |
| 249 335 | |
| 250 336 | * `maybe_filter_precond_errors` - when set to `true`, the values in |
| 251 337 | `message_by_field` instead of string become a list of error messages |
| @@ -256,15 +342,11 @@ are automatically discarded. | |
| 256 342 | back to the user. F.e. when the field's type is another struct. |
| 257 343 | Default is `false`. |
| 258 344 | |
| 259 | - [//]: # (new/2) |
| 260 | - |
| 261 | - </blockquote> |
| 345 | + <!-- new/2 --> |
| 262 346 | |
| 263 347 | ### ensure_type!/1 |
| 264 348 | |
| 265 | - <blockquote> |
| 266 | - |
| 267 | - [//]: # (ensure_type!/1) |
| 349 | + <!-- ensure_type!/1 --> |
| 268 350 | |
| 269 351 | Ensures that struct conforms to its `t()` type and all preconditions |
| 270 352 | are fulfilled. |
| @@ -274,15 +356,11 @@ Returns struct when it's valid. Raises an `ArgumentError` otherwise. | |
| 274 356 | Useful for struct validation when its fields changed with map syntax |
| 275 357 | or with `Map` module functions. |
| 276 358 | |
| 277 | - [//]: # (ensure_type!/1) |
| 278 | - |
| 279 | - </blockquote> |
| 359 | + <!-- ensure_type!/1 --> |
| 280 360 | |
| 281 361 | ### ensure_type/2/1 |
| 282 362 | |
| 283 | - <blockquote> |
| 284 | - |
| 285 | - [//]: # (ensure_type/2) |
| 363 | + <!-- ensure_type/2 --> |
| 286 364 | |
| 287 365 | Ensures that struct conforms to its `t()` type and all preconditions |
| 288 366 | are fulfilled. |
| @@ -293,24 +371,20 @@ Otherwise returns the error in the shape of `{:error, message_by_field}`. | |
| 293 371 | Useful for struct validation when its fields changed with map syntax |
| 294 372 | or with `Map` module functions. |
| 295 373 | |
| 296 | - [//]: # (ensure_type/2) |
| 297 | - |
| 298 | - </blockquote> |
| 374 | + <!-- ensure_type/2 --> |
| 299 375 | |
| 300 376 | ### typed_fields/1/0 |
| 301 377 | |
| 302 | - <blockquote> |
| 378 | + <!-- typed_fields/1 --> |
| 303 379 | |
| 304 | - [//]: # (typed_fields/1) |
| 305 | - |
| 306 | - Returns the list of struct's fields defined with its `t()` type. |
| 380 | + Returns the list of struct's fields defined with explicit types in its `t()` type spec. |
| 307 381 | |
| 308 382 | Does not return meta fields with `__underscored__` names and fields |
| 309 383 | having `any()` type by default. |
| 310 384 | |
| 311 385 | Includes fields that have `nil` type into the return list. |
| 312 386 | |
| 313 | - ## Options |
| 387 | + #### Options |
| 314 388 | |
| 315 389 | * `:include_any_typed` - when set to `true`, adds fields with `any()` |
| 316 390 | type to the return list. Default is `false`. |
| @@ -318,15 +392,11 @@ Includes fields that have `nil` type into the return list. | |
| 318 392 | * `:include_meta` - when set to `true`, adds fields |
| 319 393 | with `__underscored__` names to the return list. Default is `false`. |
| 320 394 | |
| 321 | - [//]: # (typed_fields/1) |
| 322 | - |
| 323 | - </blockquote> |
| 395 | + <!-- typed_fields/1 --> |
| 324 396 | |
| 325 397 | ### required_fields/1/0 |
| 326 398 | |
| 327 | - <blockquote> |
| 328 | - |
| 329 | - [//]: # (required_fields/1) |
| 399 | + <!-- required_fields/1 --> |
| 330 400 | |
| 331 401 | Returns the list of struct's fields having type others then `nil` or `any()`. |
| 332 402 | |
| @@ -335,14 +405,12 @@ Does not return meta fields with `__underscored__` names. | |
| 335 405 | Useful for validation of the required fields for emptiness. |
| 336 406 | F.e. with `validate_required/2` call in the `Ecto` changeset. |
| 337 407 | |
| 338 | - ## Options |
| 408 | + #### Options |
| 339 409 | |
| 340 410 | * `:include_meta` - when set to `true`, adds fields |
| 341 411 | with `__underscored__` names to the return list. Default is `false`. |
| 342 412 | |
| 343 | - [//]: # (required_fields/1) |
| 344 | - |
| 345 | - </blockquote> |
| 413 | + <!-- required_fields/1 --> |
| 346 414 | |
| 347 415 | ## Limitations |
| 348 416 | |
| @@ -389,64 +457,66 @@ with correct states at every update that is valid in many business contexts. | |
| 389 457 | |
| 390 458 | Please, find the output of `mix benchmark` command below. |
| 391 459 | |
| 392 | - Generate 10000 inputs, may take a while. |
| 393 | - ========================================= |
| 460 | + ``` |
| 461 | + Generate 10000 inputs, may take a while. |
| 462 | + ========================================= |
| 394 463 | |
| 395 | - Construction of a struct |
| 396 | - ========================================= |
| 397 | - Operating System: macOS |
| 398 | - CPU Information: Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz |
| 399 | - Number of Available Cores: 8 |
| 400 | - Available memory: 16 GB |
| 401 | - Elixir 1.12.3 |
| 402 | - Erlang 24.0.1 |
| 464 | + Construction of a struct |
| 465 | + ========================================= |
| 466 | + Operating System: macOS |
| 467 | + CPU Information: Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz |
| 468 | + Number of Available Cores: 8 |
| 469 | + Available memory: 16 GB |
| 470 | + Elixir 1.12.3 |
| 471 | + Erlang 24.0.1 |
| 403 472 | |
| 404 | - Benchmark suite executing with the following configuration: |
| 405 | - warmup: 2 s |
| 406 | - time: 5 s |
| 407 | - memory time: 0 ns |
| 408 | - parallel: 1 |
| 409 | - inputs: none specified |
| 410 | - Estimated total run time: 14 s |
| 473 | + Benchmark suite executing with the following configuration: |
| 474 | + warmup: 2 s |
| 475 | + time: 5 s |
| 476 | + memory time: 0 ns |
| 477 | + parallel: 1 |
| 478 | + inputs: none specified |
| 479 | + Estimated total run time: 14 s |
| 411 480 | |
| 412 | - Benchmarking __MODULE__.new!(arg)... |
| 413 | - Benchmarking struct!(__MODULE__, arg)... |
| 481 | + Benchmarking __MODULE__.new!(arg)... |
| 482 | + Benchmarking struct!(__MODULE__, arg)... |
| 414 483 | |
| 415 | - Name ips average deviation median 99th % |
| 416 | - struct!(__MODULE__, arg) 14.09 K 70.96 μs ±63.01% 72 μs 158 μs |
| 417 | - __MODULE__.new!(arg) 11.77 K 84.93 μs ±53.72% 87 μs 181 μs |
| 484 | + Name ips average deviation median 99th % |
| 485 | + struct!(__MODULE__, arg) 14.09 K 70.96 μs ±63.01% 72 μs 158 μs |
| 486 | + __MODULE__.new!(arg) 11.77 K 84.93 μs ±53.72% 87 μs 181 μs |
| 418 487 | |
| 419 | - Comparison: |
| 420 | - struct!(__MODULE__, arg) 14.09 K |
| 421 | - __MODULE__.new!(arg) 11.77 K - 1.20x slower +13.97 ÎĽs |
| 488 | + Comparison: |
| 489 | + struct!(__MODULE__, arg) 14.09 K |
| 490 | + __MODULE__.new!(arg) 11.77 K - 1.20x slower +13.97 ÎĽs |
| 422 491 | |
| 423 | - A struct's field modification |
| 424 | - ========================================= |
| 425 | - Operating System: macOS |
| 426 | - CPU Information: Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz |
| 427 | - Number of Available Cores: 8 |
| 428 | - Available memory: 16 GB |
| 429 | - Elixir 1.12.3 |
| 430 | - Erlang 24.0.1 |
| 492 | + A struct's field modification |
| 493 | + ========================================= |
| 494 | + Operating System: macOS |
| 495 | + CPU Information: Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz |
| 496 | + Number of Available Cores: 8 |
| 497 | + Available memory: 16 GB |
| 498 | + Elixir 1.12.3 |
| 499 | + Erlang 24.0.1 |
| 431 500 | |
| 432 | - Benchmark suite executing with the following configuration: |
| 433 | - warmup: 2 s |
| 434 | - time: 5 s |
| 435 | - memory time: 0 ns |
| 436 | - parallel: 1 |
| 437 | - inputs: none specified |
| 438 | - Estimated total run time: 14 s |
| 501 | + Benchmark suite executing with the following configuration: |
| 502 | + warmup: 2 s |
| 503 | + time: 5 s |
| 504 | + memory time: 0 ns |
| 505 | + parallel: 1 |
| 506 | + inputs: none specified |
| 507 | + Estimated total run time: 14 s |
| 439 508 | |
| 440 | - Benchmarking %{tweet | user: arg} |> __MODULE__.ensure_type!()... |
| 441 | - Benchmarking struct!(tweet, user: arg)... |
| 509 | + Benchmarking %{tweet | user: arg} |> __MODULE__.ensure_type!()... |
| 510 | + Benchmarking struct!(tweet, user: arg)... |
| 442 511 | |
| 443 | - Name ips average deviation median 99th % |
| 444 | - struct!(tweet, user: arg) 15.01 K 66.62 μs ±66.93% 70 μs 148 μs |
| 445 | - %{tweet | user: arg} |> __MODULE__.ensure_type!() 13.53 K 73.89 μs ±60.83% 75 μs 159 μs |
| 512 | + Name ips average deviation median 99th % |
| 513 | + struct!(tweet, user: arg) 15.01 K 66.62 μs ±66.93% 70 μs 148 μs |
| 514 | + %{tweet | user: arg} |> __MODULE__.ensure_type!() 13.53 K 73.89 μs ±60.83% 75 μs 159 μs |
| 446 515 | |
| 447 | - Comparison: |
| 448 | - struct!(tweet, user: arg) 15.01 K |
| 449 | - %{tweet | user: arg} |> __MODULE__.ensure_type!() 13.53 K - 1.11x slower +7.27 ÎĽs |
| 516 | + Comparison: |
| 517 | + struct!(tweet, user: arg) 15.01 K |
| 518 | + %{tweet | user: arg} |> __MODULE__.ensure_type!() 13.53 K - 1.11x slower +7.27 ÎĽs |
| 519 | + ``` |
| 450 520 | |
| 451 521 | ## Contributing |
| 452 522 | |
| @@ -454,19 +524,40 @@ Please, find the output of `mix benchmark` command below. | |
| 454 524 | |
| 455 525 | 2. After implementing of the feature format the code with: |
| 456 526 | |
| 457 | - mix format |
| 527 | + ``` |
| 528 | + mix format |
| 529 | + ``` |
| 458 530 | |
| 459 531 | run linter and tests to ensure that all works as expected with: |
| 460 532 | |
| 461 | - mix check || mix check --failed |
| 533 | + ``` |
| 534 | + mix check || mix check --failed |
| 535 | + ``` |
| 462 536 | |
| 463 537 | 3. Make a PR to this repository |
| 464 538 | |
| 465 539 | ## Changelog |
| 466 540 | |
| 541 | + ### 1.5.0 |
| 542 | + |
| 543 | + * Fix bug to return explicit file read error message during the compile time |
| 544 | + |
| 545 | + * Completely replace `apply()` with `.` for validation function calls to run faster |
| 546 | + |
| 547 | + * Link planner server to mix process for better state handling |
| 548 | + |
| 549 | + * Support of the interactive use in `iex` and `live book` |
| 550 | + |
| 551 | + Breaking change: |
| 552 | + |
| 553 | + * Improve compilation speed by starting resolve planner only once in Domo mix task. |
| 554 | + To migrate, please, put the `:domo_compiler` before `:elixir` in mix.exs. |
| 555 | + And do the same for `reloadable_compilers` key in config file |
| 556 | + if configured for Phoenix endpoint. |
| 557 | + |
| 467 558 | ### 1.4.1 |
| 468 559 | |
| 469 | - * Adaptions for Elixir v1.13 |
| 560 | + * Improve compatibility with Elixir v1.13 |
| 470 561 | |
| 471 562 | * Format string representations of an anonymous function passed to `precond/1` macro error message |
| 472 563 | |
| @@ -486,9 +577,10 @@ Breaking changes: | |
| 486 577 | to the value of `:name_of_new_function` option. The defaults are `new` and `new!`. |
| 487 578 | |
| 488 579 | ### 1.3.4 |
| 580 | + |
| 489 581 | * Make error messages to be more informative |
| 490 582 | |
| 491 | - * Improve compatibility with `Ecto` 3.7.x |
| 583 | + * Improve compatibility with `Ecto` 3.7.x |
| 492 584 | |
| 493 585 | * Explicitly define `:ecto` and `:decimal` as optional dependencies |
| 494 586 | |
| @@ -499,11 +591,13 @@ Breaking changes: | |
| 499 591 | * Replace `apply()` with Module.function calls to run faster |
| 500 592 | |
| 501 593 | ### 1.3.3 |
| 594 | + |
| 502 595 | * Support validation of `Decimal.t()` |
| 503 596 | |
| 504 597 | * Fix bug to define precondition function for user type referencing any() or term() |
| 505 598 | |
| 506 599 | ### 1.3.2 |
| 600 | + |
| 507 601 | * Support remote types in erlang modules like `:inet.port_number()` |
| 508 602 | |
| 509 603 | * Shorten the invalid value output in the error message |
| @@ -523,9 +617,11 @@ Breaking changes: | |
| 523 617 | * Add example of parsing with validating of the Contentful JSON reply via `Jason` + `ExJSONPath` + `Domo` |
| 524 618 | |
| 525 619 | ### 1.3.1 |
| 620 | + |
| 526 621 | * Fix bug to validate defaults having | nil type. |
| 527 622 | |
| 528 623 | ### 1.3.0 |
| 624 | + |
| 529 625 | * Change the default name of the constructor function to `new!` to follow Elixir naming convention. |
| 530 626 | You can always change the name with the `config :domo, :name_of_new_function, :new_func_name_here` app configuration. |
| 531 627 | |
| @@ -536,6 +632,7 @@ Breaking changes: | |
| 536 632 | * Add examples of integrations with `TypedStruct` and `TypedEctoSchema`. |
| 537 633 | |
| 538 634 | ### 1.2.9 |
| 635 | + |
| 539 636 | * Fix bug to acknowledge that type has been changed after a failed compilation. |
| 540 637 | |
| 541 638 | * Fix bug to match structs not using Domo with a field of `any()` type with and without precondition. |
| @@ -545,26 +642,31 @@ Breaking changes: | |
| 545 642 | * Add `maybe_filter_precond_errors: true` option that filters errors from precondition functions for better output for the user. |
| 546 643 | |
| 547 644 | ### 1.2.8 |
| 645 | + |
| 548 646 | * Add `Domo.Changeset.validate_type/*` functions to validate Echo.Changeset field changes matching the t() type. |
| 549 647 | |
| 550 648 | * Fix the bug to return custom error from precondition function as underlying error for :| types. |
| 551 649 | |
| 552 650 | ### 1.2.7 |
| 651 | + |
| 553 652 | * Fix the bug to make recompilation occur when fixing alias for remote type. |
| 554 653 | |
| 555 654 | * Support custom errors to be returned from functions defined with `precond/1`. |
| 556 655 | |
| 557 656 | ### 1.2.6 |
| 657 | + |
| 558 658 | * Validates type conformance of default values given with `defstruct/1` to the |
| 559 659 | struct's `t()` type at compile-time. |
| 560 660 | |
| 561 661 | * Includes only the most matching type error into the error message. |
| 562 662 | |
| 563 663 | ### 1.2.5 |
| 664 | + |
| 564 665 | * Add `remote_types_as_any` option to disable validation of specified complex |
| 565 666 | remote types. What can be replaced by precondition for wrapping user-defined type. |
| 566 667 | |
| 567 668 | ### 1.2.4 |
| 669 | + |
| 568 670 | * Speedup resolving of struct types |
| 569 671 | * Limit the number of allowed fields types combinations to 4096 |
| 570 672 | * Support `Range.t()` and `MapSet.t()` |
| @@ -573,52 +675,52 @@ Breaking changes: | |
| 573 675 | * List processed structs giving mix `--verbose` option |
| 574 676 | |
| 575 677 | ### 1.2.3 |
| 678 | + |
| 576 679 | * Support struct's attribute introduced in Elixir 1.12.0 for error checking |
| 577 680 | * Add user-defined precondition functions to check the allowed range of values |
| 578 681 | with `precond/1` macro |
| 579 682 | |
| 580 683 | ### 1.2.2 |
| 684 | + |
| 581 685 | * Add support for `new/1` calls at compile time f.e. to specify default values |
| 582 686 | |
| 583 687 | ### 1.2.1 |
| 688 | + |
| 584 689 | * Domo compiler is renamed to `:domo_compiler` |
| 585 | - * Compile `TypeEnsurer` modules only if struct changes or dependency type changes |
| 690 | + * Compile `TypeEnsurer` modules only if struct changes or dependency type changes |
| 586 691 | * Phoenix hot-reload with `:reloadable_compilers` option is fully supported |
| 587 692 | |
| 588 | - ### 1.2.0 |
| 693 | + ### 1.2.0 |
| 694 | + |
| 589 695 | * Resolve all types at compile time and build `TypeEnsurer` modules for all structs |
| 590 696 | * Make Domo library work with Elixir 1.11.x and take it as the required minimum version |
| 591 697 | * Introduce `---/2` operator to make tag chains with `Domo.TaggedTuple` module |
| 592 698 | |
| 593 | - ### 0.0.x - 1.0.x |
| 699 | + ### 0.0.x - 1.0.x |
| 700 | + |
| 594 701 | * MVP like releases, resolving types at runtime. Adds `new` constructor to a struct |
| 595 702 | |
| 596 703 | ## Roadmap |
| 597 704 | |
| 598 | - * [x] Check if the field values passed as an argument to the `new/1`, |
| 599 | - and `put/3` matches the field types defined in `typedstruct/1`. |
| 705 | + ## Roadmap |
| 706 | + |
| 707 | + * [x] Check if the field values passed as an argument to the `new/1`, and `put/3` matches the field types defined in `typedstruct/1`. |
| 600 708 | |
| 601 709 | * [x] Support the keyword list as a possible argument for the `new/1`. |
| 602 710 | |
| 603 | - * [x] Add module option to put a warning in the console instead of raising |
| 604 | - of the `ArgumentError` exception on value type mismatch. |
| 711 | + * [x] Add module option to put a warning in the console instead of raising of the `ArgumentError` exception on value type mismatch. |
| 605 712 | |
| 606 | - * [x] Make global environment configuration options to turn errors into |
| 607 | - warnings that are equivalent to module ones. |
| 713 | + * [x] Make global environment configuration options to turn errors into warnings that are equivalent to module ones. |
| 608 714 | |
| 609 715 | * [x] Move type resolving to the compile time. |
| 610 716 | |
| 611 | - * [x] Keep only bare minimum of generated functions that are `new/1`, |
| 612 | - `ensure_type!/1` and their _ok versions. |
| 717 | + * [x] Keep only bare minimum of generated functions that are `new/1`, `ensure_type!/1` and their _ok versions. |
| 613 718 | |
| 614 | - * [x] Make the `new/1` and `ensure_type!/1` speed to be less or equal |
| 615 | - to 1.5 times of the `struct!/2` speed. |
| 719 | + * [x] Make the `new/1` and `ensure_type!/1` speed to be less or equal to 1.5 times of the `struct!/2` speed. |
| 616 720 | |
| 617 | - * [x] Support `new/1` calls in macros to specify default values f.e. in other |
| 618 | - structures. That is to check if default value matches type at compile time. |
| 721 | + * [x] Support `new/1` calls in macros to specify default values f.e. in other structures. That is to check if default value matches type at compile time. |
| 619 722 | |
| 620 | - * [x] Support `precond/1` macro to specify a struct field value's contract |
| 621 | - with a boolean function. |
| 723 | + * [x] Support `precond/1` macro to specify a struct field value's contract with a boolean function. |
| 622 724 | |
| 623 725 | * [ ] Support types referencing itself for tree structures. |
| 624 726 | |
| @@ -626,9 +728,7 @@ Breaking changes: | |
| 626 728 | |
| 627 729 | * [x] Add use option to specify names of the generated functions. |
| 628 730 | |
| 629 | - * [x] Add documentation to the generated for `new(_ok)/1`, and `ensure_type!(_ok)/1` |
| 630 | - functions in a struct. |
| 631 | - |
| 731 | + * [x] Add documentation to the generated for `new(_ok)/1`, and `ensure_type!(_ok)/1` functions in a struct. |
| 632 732 | |
| 633 733 | ## License |
| @@ -6,7 +6,8 @@ | |
| 6 6 | {<<"files">>, |
| 7 7 | [<<".formatter.exs">>,<<"lib">>,<<"lib/domo">>, |
| 8 8 | <<"lib/domo/error_builder.ex">>,<<"lib/domo/changeset.ex">>, |
| 9 | - <<"lib/domo/mix_project_helper.ex">>,<<"lib/domo/raises.ex">>, |
| 9 | + <<"lib/domo/code_evaluation.ex">>, |
| 10 | + <<"lib/domo/interactive_type_registration.ex">>,<<"lib/domo/raises.ex">>, |
| 10 11 | <<"lib/domo/precondition_handler.ex">>,<<"lib/domo/type_ensurer_factory">>, |
| 11 12 | <<"lib/domo/type_ensurer_factory/batch_ensurer.ex">>, |
| 12 13 | <<"lib/domo/type_ensurer_factory/atomizer.ex">>, |
| @@ -51,4 +52,4 @@ | |
| 51 52 | {<<"optional">>,true}, |
| 52 53 | {<<"repository">>,<<"hexpm">>}, |
| 53 54 | {<<"requirement">>,<<">= 0.0.0">>}]]}. |
| 54 | - {<<"version">>,<<"1.4.1">>}. |
| 55 | + {<<"version">>,<<"1.5.0">>}. |
| @@ -1,12 +1,12 @@ | |
| 1 1 | defmodule Domo do |
| 2 | - @moduledoc Domo.Doc.readme_doc("[//]: # (Documentation)") |
| 2 | + @moduledoc Domo.Doc.readme_doc("<!-- Documentation -->") |
| 3 3 | |
| 4 | - @new_raise_doc Domo.Doc.readme_doc("[//]: # (new!/1)") |
| 5 | - @new_ok_doc Domo.Doc.readme_doc("[//]: # (new/2)") |
| 6 | - @ensure_type_raise_doc Domo.Doc.readme_doc("[//]: # (ensure_type!/1)") |
| 7 | - @ensure_type_ok_doc Domo.Doc.readme_doc("[//]: # (ensure_type/2)") |
| 8 | - @typed_fields_doc Domo.Doc.readme_doc("[//]: # (typed_fields/1)") |
| 9 | - @required_fields_doc Domo.Doc.readme_doc("[//]: # (required_fields/1)") |
| 4 | + @new_raise_doc Domo.Doc.readme_doc("<!-- new!/1 -->") |
| 5 | + @new_ok_doc Domo.Doc.readme_doc("<!-- new/2 -->") |
| 6 | + @ensure_type_raise_doc Domo.Doc.readme_doc("<!-- ensure_type!/1 -->") |
| 7 | + @ensure_type_ok_doc Domo.Doc.readme_doc("<!-- ensure_type/2 -->") |
| 8 | + @typed_fields_doc Domo.Doc.readme_doc("<!-- typed_fields/1 -->") |
| 9 | + @required_fields_doc Domo.Doc.readme_doc("<!-- required_fields/1 -->") |
| 10 10 | |
| 11 11 | @callback new!() :: struct() |
| 12 12 | @doc @new_raise_doc |
| @@ -27,10 +27,13 @@ defmodule Domo do | |
| 27 27 | @doc @required_fields_doc |
| 28 28 | @callback required_fields(opts :: keyword()) :: [atom()] |
| 29 29 | |
| 30 | + @mix_project Application.compile_env(:domo, :mix_project, Mix.Project) |
| 31 | + |
| 30 32 | alias Domo.ErrorBuilder |
| 31 | - alias Domo.MixProjectHelper |
| 33 | + alias Domo.CodeEvaluation |
| 32 34 | alias Domo.Raises |
| 33 35 | alias Domo.TypeEnsurerFactory |
| 36 | + alias Domo.TypeEnsurerFactory.Error |
| 34 37 | alias Mix.Tasks.Compile.DomoCompiler, as: DomoMixTask |
| 35 38 | |
| 36 39 | @doc """ |
| @@ -94,23 +97,41 @@ defmodule Domo do | |
| 94 97 | |
| 95 98 | The option value given to the macro overrides one set globally in the |
| 96 99 | configuration with `config :domo, option: value`. |
| 100 | + |
| 101 | + Run the `Application.put_env(:domo, :verbose_in_iex, true)` to enable verbose |
| 102 | + messages from domo in Interactive Elixir console. |
| 97 103 | """ |
| 98 104 | # credo:disable-for-lines:332 |
| 99 105 | defmacro __using__(opts) do |
| 100 106 | Raises.raise_use_domo_out_of_module!(__CALLER__) |
| 101 | - Raises.raise_absence_of_domo_compiler!(Mix.Project.config(), opts, __CALLER__) |
| 102 107 | |
| 103 | - do_test_env_ckeck = |
| 104 | - case Application.fetch_env(:domo, :skip_test_env_check) do |
| 105 | - {:ok, true} -> false |
| 106 | - _ -> true |
| 108 | + in_mix_compile? = CodeEvaluation.in_mix_compile?(__CALLER__) |
| 109 | + config = @mix_project.config() |
| 110 | + |
| 111 | + if in_mix_compile? do |
| 112 | + Raises.maybe_raise_absence_of_domo_compiler!(config, __CALLER__) |
| 113 | + else |
| 114 | + do_test_env_ckeck = |
| 115 | + case Application.fetch_env(:domo, :skip_test_env_check) do |
| 116 | + {:ok, true} -> false |
| 117 | + _ -> true |
| 118 | + end |
| 119 | + |
| 120 | + if do_test_env_ckeck and CodeEvaluation.in_mix_test?(__CALLER__) do |
| 121 | + Raises.raise_cant_build_in_test_environment(__CALLER__.module) |
| 107 122 | end |
| 108 123 | |
| 109 | - if do_test_env_ckeck and not is_nil(GenServer.whereis(ExUnit.Server)) do |
| 110 | - Raises.raise_cant_build_in_test_environment(__CALLER__.module) |
| 124 | + # We consider to be in interactive mode |
| 125 | + opts = [verbose?: Application.get_env(:domo, :verbose_in_iex, false)] |
| 126 | + TypeEnsurerFactory.start_resolve_planner(:in_memory, :in_memory, opts) |
| 111 127 | end |
| 112 128 | |
| 113 | - start_resolve_planner() |
| 129 | + maybe_build_type_ensurer_after_compile = |
| 130 | + unless in_mix_compile? do |
| 131 | + quote do |
| 132 | + @after_compile {Domo, :_build_in_memory_type_ensurer} |
| 133 | + end |
| 134 | + end |
| 114 135 | |
| 115 136 | global_anys = |
| 116 137 | if global_anys = Application.get_env(:domo, :remote_types_as_any) do |
| @@ -124,8 +145,14 @@ defmodule Domo do | |
| 124 145 | Enum.map(local_anys, fn {module, types} -> {Macro.expand_once(module, __CALLER__), types} end) |
| 125 146 | end |
| 126 147 | |
| 148 | + plan_path = |
| 149 | + if in_mix_compile? do |
| 150 | + DomoMixTask.manifest_path(@mix_project, :plan) |
| 151 | + else |
| 152 | + :in_memory |
| 153 | + end |
| 154 | + |
| 127 155 | unless is_nil(global_anys) and is_nil(local_anys) do |
| 128 | - plan_path = get_plan_path() |
| 129 156 | TypeEnsurerFactory.collect_types_to_treat_as_any(plan_path, __CALLER__.module, global_anys, local_anys) |
| 130 157 | end |
| 131 158 | |
| @@ -140,14 +167,18 @@ defmodule Domo do | |
| 140 167 | |> Enum.join() |
| 141 168 | |> String.to_atom() |
| 142 169 | |
| 143 | - type_ensurer = TypeEnsurerFactory.type_ensurer(__CALLER__.module) |
| 144 | - |
| 145 170 | long_module = TypeEnsurerFactory.module_name_string(__CALLER__.module) |
| 146 171 | short_module = long_module |> String.split(".") |> List.last() |
| 147 172 | |
| 173 | + type_ensurer = TypeEnsurerFactory.type_ensurer(__CALLER__.module) |
| 174 | + |
| 148 175 | quote do |
| 149 176 | Module.register_attribute(__MODULE__, :domo_options, accumulate: false) |
| 150 177 | Module.put_attribute(__MODULE__, :domo_options, unquote(opts)) |
| 178 | + Module.register_attribute(__MODULE__, :domo_plan_path, accumulate: false) |
| 179 | + Module.put_attribute(__MODULE__, :domo_plan_path, unquote(plan_path)) |
| 180 | + |
| 181 | + @compile {:no_warn_undefined, unquote(type_ensurer)} |
| 151 182 | |
| 152 183 | import Domo, only: [precond: 1] |
| 153 184 | |
| @@ -162,7 +193,7 @@ defmodule Domo do | |
| 162 193 | """ |
| 163 194 | def unquote(new_raise_fun_name)(enumerable \\ []) do |
| 164 195 | skip_ensurance? = |
| 165 | - if TypeEnsurerFactory.compile_time?() do |
| 196 | + if CodeEvaluation.in_plan_collection?() do |
| 166 197 | Domo._plan_struct_integrity_ensurance(__MODULE__, enumerable) |
| 167 198 | true |
| 168 199 | else |
| @@ -172,8 +203,6 @@ defmodule Domo do | |
| 172 203 | struct = struct!(__MODULE__, enumerable) |
| 173 204 | |
| 174 205 | unless skip_ensurance? do |
| 175 | - Raises.maybe_raise_add_domo_compiler(__MODULE__) |
| 176 | - |
| 177 206 | {errors, t_precondition_error} = Domo._do_validate_fields(unquote(type_ensurer), struct, :pretty_error) |
| 178 207 | |
| 179 208 | unless Enum.empty?(errors) do |
| @@ -199,7 +228,7 @@ defmodule Domo do | |
| 199 228 | """ |
| 200 229 | def unquote(new_ok_fun_name)(enumerable \\ [], opts \\ []) do |
| 201 230 | skip_ensurance? = |
| 202 | - if TypeEnsurerFactory.compile_time?() do |
| 231 | + if CodeEvaluation.in_plan_collection?() do |
| 203 232 | Domo._plan_struct_integrity_ensurance(__MODULE__, enumerable) |
| 204 233 | true |
| 205 234 | else |
| @@ -211,8 +240,6 @@ defmodule Domo do | |
| 211 240 | if skip_ensurance? do |
| 212 241 | {:ok, struct} |
| 213 242 | else |
| 214 | - Raises.maybe_raise_add_domo_compiler(__MODULE__) |
| 215 | - |
| 216 243 | {errors, t_precondition_error} = Domo._do_validate_fields(unquote(type_ensurer), struct, :pretty_error_by_key, opts) |
| 217 244 | |
| 218 245 | cond do |
| @@ -247,7 +274,7 @@ defmodule Domo do | |
| 247 274 | end |
| 248 275 | |
| 249 276 | skip_ensurance? = |
| 250 | - if TypeEnsurerFactory.compile_time?() do |
| 277 | + if CodeEvaluation.in_plan_collection?() do |
| 251 278 | Domo._plan_struct_integrity_ensurance(__MODULE__, Map.from_struct(struct)) |
| 252 279 | true |
| 253 280 | else |
| @@ -255,8 +282,6 @@ defmodule Domo do | |
| 255 282 | end |
| 256 283 | |
| 257 284 | unless skip_ensurance? do |
| 258 | - Raises.maybe_raise_add_domo_compiler(__MODULE__) |
| 259 | - |
| 260 285 | {errors, t_precondition_error} = Domo._do_validate_fields(unquote(type_ensurer), struct, :pretty_error) |
| 261 286 | |
| 262 287 | unless Enum.empty?(errors) do |
| @@ -299,7 +324,7 @@ defmodule Domo do | |
| 299 324 | end |
| 300 325 | |
| 301 326 | skip_ensurance? = |
| 302 | - if TypeEnsurerFactory.compile_time?() do |
| 327 | + if CodeEvaluation.in_plan_collection?() do |
| 303 328 | Domo._plan_struct_integrity_ensurance(__MODULE__, Map.from_struct(struct)) |
| 304 329 | true |
| 305 330 | else |
| @@ -309,7 +334,6 @@ defmodule Domo do | |
| 309 334 | if skip_ensurance? do |
| 310 335 | {:ok, struct} |
| 311 336 | else |
| 312 | - Raises.maybe_raise_add_domo_compiler(__MODULE__) |
| 313 337 | Domo._validate_fields_ok(unquote(type_ensurer), struct, opts) |
| 314 338 | end |
| 315 339 | end |
| @@ -324,13 +348,13 @@ defmodule Domo do | |
| 324 348 | true -> :typed_no_meta_no_any |
| 325 349 | end |
| 326 350 | |
| 327 | - apply(unquote(type_ensurer), :fields, [field_kind]) |
| 351 | + unquote(type_ensurer).fields(field_kind) |
| 328 352 | end |
| 329 353 | |
| 330 354 | @doc unquote(@required_fields_doc) |
| 331 355 | def required_fields(opts \\ []) do |
| 332 356 | field_kind = if opts[:include_meta], do: :required_with_meta, else: :required_no_meta |
| 333 | - apply(unquote(type_ensurer), :fields, [field_kind]) |
| 357 | + unquote(type_ensurer).fields(field_kind) |
| 334 358 | end |
| 335 359 | |
| 336 360 | @before_compile {Raises, :raise_not_in_a_struct_module!} |
| @@ -338,43 +362,55 @@ defmodule Domo do | |
| 338 362 | @before_compile {Domo, :_plan_struct_defaults_ensurance} |
| 339 363 | |
| 340 364 | @after_compile {Domo, :_collect_types_for_domo_compiler} |
| 365 | + unquote(maybe_build_type_ensurer_after_compile) |
| 341 366 | end |
| 342 367 | end |
| 343 368 | |
| 344 | - defp start_resolve_planner do |
| 345 | - plan_path = get_plan_path() |
| 346 | - preconds_path = get_precond_path() |
| 347 | - |
| 348 | - {:ok, _pid} = TypeEnsurerFactory.start_resolve_planner(plan_path, preconds_path) |
| 349 | - |
| 350 | - plan_path |
| 351 | - end |
| 352 | - |
| 353 | - defp get_plan_path do |
| 354 | - project = MixProjectHelper.global_stub() || Mix.Project |
| 355 | - DomoMixTask.manifest_path(project, :plan) |
| 356 | - end |
| 357 | - |
| 358 | - defp get_precond_path do |
| 359 | - project = MixProjectHelper.global_stub() || Mix.Project |
| 360 | - DomoMixTask.manifest_path(project, :preconds) |
| 361 | - end |
| 362 | - |
| 363 369 | @doc false |
| 364 370 | def _plan_struct_defaults_ensurance(env) do |
| 365 | - plan_path = get_plan_path() |
| 371 | + plan_path = Module.get_attribute(env.module, :domo_plan_path) |
| 366 372 | TypeEnsurerFactory.plan_struct_defaults_ensurance(plan_path, env) |
| 367 373 | end |
| 368 374 | |
| 369 375 | @doc false |
| 370 376 | def _collect_types_for_domo_compiler(env, bytecode) do |
| 371 | - plan_path = get_plan_path() |
| 377 | + plan_path = Module.get_attribute(env.module, :domo_plan_path) |
| 372 378 | TypeEnsurerFactory.collect_types_for_domo_compiler(plan_path, env, bytecode) |
| 373 379 | end |
| 374 380 | |
| 381 | + @doc false |
| 382 | + def _build_in_memory_type_ensurer(env, bytecode) do |
| 383 | + verbose? = Application.get_env(:domo, :verbose_in_iex, false) |
| 384 | + |
| 385 | + TypeEnsurerFactory.register_in_memory_types(env.module, bytecode) |
| 386 | + # struct's types are collected with separate _collect_types_for_domo_compiler call |
| 387 | + TypeEnsurerFactory.maybe_collect_types_for_stdlib_structs(:in_memory) |
| 388 | + TypeEnsurerFactory.maybe_collect_lib_structs_to_treat_as_any_to_existing_plan(:in_memory) |
| 389 | + |
| 390 | + {:ok, plan, preconds} = TypeEnsurerFactory.get_plan_state(:in_memory) |
| 391 | + |
| 392 | + with {:ok, module_filed_types, dependencies_by_module} <- TypeEnsurerFactory.resolve_plan(plan, preconds, verbose?), |
| 393 | + TypeEnsurerFactory.build_type_ensurers(module_filed_types, verbose?), |
| 394 | + :ok <- TypeEnsurerFactory.ensure_struct_defaults(plan, verbose?) do |
| 395 | + {:ok, dependants} = TypeEnsurerFactory.get_dependants(:in_memory, env.module) |
| 396 | + |
| 397 | + unless dependants == [] do |
| 398 | + TypeEnsurerFactory.invalidate_type_ensurers(dependants) |
| 399 | + Raises.warn_invalidated_type_ensurers(env.module, dependants) |
| 400 | + end |
| 401 | + |
| 402 | + TypeEnsurerFactory.register_dependants_from(:in_memory, dependencies_by_module) |
| 403 | + TypeEnsurerFactory.clean_plan(:in_memory) |
| 404 | + :ok |
| 405 | + else |
| 406 | + {:error, [%Error{message: {:no_types_registered, _} = error}]} -> Raises.raise_cant_find_type_in_memory(error) |
| 407 | + {:error, {:batch_ensurer, _details} = message} -> Raises.raise_incorrect_defaults(message) |
| 408 | + end |
| 409 | + end |
| 410 | + |
| 375 411 | @doc false |
| 376 412 | def _plan_struct_integrity_ensurance(module, enumerable) do |
| 377 | - plan_path = get_plan_path() |
| 413 | + plan_path = DomoMixTask.manifest_path(@mix_project, :plan) |
| 378 414 | TypeEnsurerFactory.plan_struct_integrity_ensurance(plan_path, module, enumerable) |
| 379 415 | end |
| 380 416 | |
| @@ -391,13 +427,13 @@ defmodule Domo do | |
| 391 427 | |
| 392 428 | def _do_validate_fields(type_ensurer, struct, err_fun, opts \\ []) do |
| 393 429 | maybe_filter_precond_errors = Keyword.get(opts, :maybe_filter_precond_errors, false) |
| 394 | - typed_no_any_fields = apply(type_ensurer, :fields, [:typed_with_meta_no_any]) |
| 430 | + typed_no_any_fields = type_ensurer.fields(:typed_with_meta_no_any) |
| 395 431 | |
| 396 432 | errors = |
| 397 433 | Enum.reduce(typed_no_any_fields, [], fn field, errors -> |
| 398 434 | field_value = {field, Map.get(struct, field)} |
| 399 435 | |
| 400 | - case apply(type_ensurer, :ensure_field_type, [field_value]) do |
| 436 | + case type_ensurer.ensure_field_type(field_value) do |
| 401 437 | {:error, _} = error -> |
| 402 438 | [apply(ErrorBuilder, err_fun, [error, maybe_filter_precond_errors]) | errors] |
| 403 439 | |
| @@ -408,7 +444,7 @@ defmodule Domo do | |
| 408 444 | |
| 409 445 | t_precondition_error = |
| 410 446 | if Enum.empty?(errors) do |
| 411 | - case apply(type_ensurer, :t_precondition, [struct]) do |
| 447 | + case type_ensurer.t_precondition(struct) do |
| 412 448 | {:error, _} = error -> apply(ErrorBuilder, err_fun, [error, maybe_filter_precond_errors]) |
| 413 449 | :ok -> nil |
| 414 450 | end |
| @@ -486,7 +522,7 @@ defmodule Domo do | |
| 486 522 | |
| 487 523 | quote do |
| 488 524 | def __precond__(unquote(type_name), value) do |
| 489 | - apply(unquote(fun), [value]) |
| 525 | + unquote(fun).(value) |
| 490 526 | end |
| 491 527 | end |
| 492 528 | end |
| @@ -497,8 +533,20 @@ defmodule Domo do | |
| 497 533 | |
| 498 534 | @doc false |
| 499 535 | def _plan_precond_checks(env, bytecode) do |
| 500 | - # precond macro can be called via import Domo, so need to start resolve planner |
| 501 | - plan_path = start_resolve_planner() |
| 536 | + in_mix_compile? = CodeEvaluation.in_mix_compile?(env) |
| 537 | + |
| 538 | + if in_mix_compile? do |
| 539 | + config = @mix_project.config() |
| 540 | + Raises.maybe_raise_absence_of_domo_compiler!(config, env) |
| 541 | + end |
| 542 | + |
| 543 | + plan_path = |
| 544 | + if in_mix_compile? do |
| 545 | + DomoMixTask.manifest_path(@mix_project, :plan) |
| 546 | + else |
| 547 | + :in_memory |
| 548 | + end |
| 549 | + |
| 502 550 | TypeEnsurerFactory.plan_precond_checks(plan_path, env, bytecode) |
| 503 551 | end |
| @@ -0,0 +1,25 @@ | |
| 1 | + defmodule Domo.CodeEvaluation do |
| 2 | + @moduledoc false |
| 3 | + |
| 4 | + @plan_collection_key __MODULE__.PlanCollection |
| 5 | + |
| 6 | + def in_mix_compile?(module_env) do |
| 7 | + tracers = Map.get(module_env || %{}, :tracers, []) |
| 8 | + Enum.member?(tracers, Mix.Compilers.ApplicationTracer) |
| 9 | + end |
| 10 | + |
| 11 | + def in_mix_test?(_module_env) do |
| 12 | + not is_nil(GenServer.whereis(ExUnit.Server)) |
| 13 | + end |
| 14 | + |
| 15 | + def put_plan_collection(flag) do |
| 16 | + Application.put_env(:domo, @plan_collection_key, flag, persistent: true) |
| 17 | + end |
| 18 | + |
| 19 | + def in_plan_collection? do |
| 20 | + case Application.fetch_env(:domo, @plan_collection_key) do |
| 21 | + {:ok, true} -> true |
| 22 | + _ -> false |
| 23 | + end |
| 24 | + end |
| 25 | + end |
| @@ -218,8 +218,7 @@ defmodule Domo.ErrorBuilder do | |
| 218 218 | parts |
| 219 219 | |> Enum.at(1) |
| 220 220 | |> String.split("\n") |
| 221 | - |> Enum.map(&(" " <> &1)) |
| 222 | - |> Enum.join("\n") |
| 221 | + |> Enum.map_join("\n", &(" " <> &1)) |
| 223 222 | |
| 224 223 | [error: Enum.join(["the following:", shifted_underlying], "\n")] |
| 225 224 | else |
Loading more files…