Current section

43 Versions

Jump to

Compare versions

17 files changed
+1087 additions
-561 deletions
  @@ -1,22 +1,19 @@
1 1 # Domo
2 2
3 - [![Build Status](https://travis-ci.com/IvanRublev/domo.svg?branch=master)](https://travis-ci.com/IvanRublev/domo)
4 - [![Method TDD](https://img.shields.io/badge/method-TDD-blue)](#domo)
5 - [![hex.pm version](http://img.shields.io/hexpm/v/domo.svg?style=flat)](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 + |[![Build Status](https://travis-ci.com/IvanRublev/domo.svg?branch=master)](https://travis-ci.com/IvanRublev/domo)|[![Method TDD](https://img.shields.io/badge/method-TDD-blue)](#domo)|[![hex.pm version](http://img.shields.io/hexpm/v/domo.svg?style=flat)](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…