From a7a876f055947bbadeadd011b43ad34a9164deda Mon Sep 17 00:00:00 2001 From: Ivan Rublev Date: Mon, 29 Apr 2024 23:58:30 +0200 Subject: [PATCH] Support sum type as an element of the list type --- .formatter.exs | 4 +- .github/workflows/ci.yml | 2 +- README.md | 40 +- lib/domo/error_builder.ex | 1 + .../generator/match_fun_registry.ex | 2 + .../generator/match_fun_registry/lists.ex | 78 ++-- .../match_fun_registry/or_elements.ex | 70 +++ .../generator/type_spec.ex | 4 + .../type_ensurer_factory/resolver/fields.ex | 413 ++++++++---------- mix.exs | 5 +- test/domo/changeset_test.exs | 36 +- .../type_ensurer_factory/generator_test.exs | 57 ++- .../generator_type_ensurer_module_test.exs | 62 ++- .../module_inspector_test.exs | 46 +- .../resolve_planner_in_memory_test.exs | 2 - .../resolver/basic_test.exs | 4 +- .../type_ensurer_factory/resolver/or_test.exs | 263 ++++------- .../resolver/preconds_test.exs | 82 ++-- .../resolver/remote_test.exs | 33 +- test/domo_func_test.exs | 21 +- test/domo_test.exs | 115 +++-- test/struct_modules/lib/user_types.ex | 2 +- test/support/resolver_test_helper.ex | 22 +- 23 files changed, 733 insertions(+), 631 deletions(-) create mode 100644 lib/domo/type_ensurer_factory/generator/match_fun_registry/or_elements.ex diff --git a/.formatter.exs b/.formatter.exs index 28b18bf2..92c9db0b 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -5,8 +5,8 @@ locals_without_parens = [precond: 1, allow: :*, assert_called: :*] "{mix,.iex,.formatter,.credo}.exs", "{config,lib}/**/*.{ex,exs}", "test/*.{ex,exs}", - "test/{domo,support}/*.{ex,exs}", - "test/struct_modules/lib/*.{ex,exs}" + "test/{domo,support}/**/*.{ex,exs}", + "test/struct_modules/lib/**/*.{ex,exs}" ], locals_without_parens: locals_without_parens, export: [ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49cabd1f..c9a6bcba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - otp: 24.3.3 elixir: 1.13.4 - otp: 26.0 - elixir: 1.15 + elixir: 1.16 steps: - uses: actions/checkout@v2 - uses: erlef/setup-beam@v1.15 diff --git a/README.md b/README.md index e6ec33b3..d2cc28ee 100644 --- a/README.md +++ b/README.md @@ -160,8 +160,8 @@ po {:ok, %PurchaseOrder{approved_limit: 200, id: 1000, items: [%LineItem{amount: 150}]}} ``` -Domo returns the error if the precondition function attached to the `t()` type -that validates invariants for the struct as a whole fails: +Domo returns the error if the precondition function validating the `t()` type +as a whole fails: ```elixir updated_po = %{po | items: [LineItem.new!(amount: 180), LineItem.new!(amount: 100)]} @@ -171,6 +171,37 @@ PurchaseOrder.ensure_type(updated_po) {:error, [t: "Sum of line item amounts should be <= to approved limit"]} ``` +Domo supports sum types for struct fields, for example: + +```elixir +defmodule FruitBasket do + use Domo + + defstruct fruits: [] + + @type t() :: %__MODULE__{fruits: [String.t() | :banana]} +end +``` + +```elixir +FruitBasket.new(fruits: [:banana, "Maracuja"]) +``` +```output +{:ok, %FruitBasket{fruits: [:banana, "Maracuja"]}} +``` + +```elixir +{:error, [fruits: message]} = FruitBasket.new(fruits: [:banana, "Maracuja", nil]) +IO.puts(message) +``` +```output +Invalid value [:banana, "Maracuja", nil] for field :fruits of %FruitBasket{}. Expected the value matching the [<<_::_*8>> | :banana] type. +Underlying errors: + - The element at index 2 has value nil that is invalid. + - Expected the value matching the <<_::_*8>> type. + - Expected the value matching the :banana type. +``` + Getting the list of the required fields of the struct that have type other then `nil` or `any` is like that: @@ -794,6 +825,11 @@ Domo compiled validation functions for the given struct based on the described t ## Changelog +## v1.5.15 + +* Support sum types as element of a list: [a | b] +* Improve compatibility with Elixir 1.16 + ## v1.5.14 * Fix validate_type/* function to run without Ecto validate_required warning. diff --git a/lib/domo/error_builder.ex b/lib/domo/error_builder.ex index 5b96197e..e1476cd7 100644 --- a/lib/domo/error_builder.ex +++ b/lib/domo/error_builder.ex @@ -78,6 +78,7 @@ defmodule Domo.ErrorBuilder do end def pretty_error({:error, {:type_mismatch, struct_module, field, value, expected_types, error_templates}}, filter_preconds?, bypass_preconds?) do + error_templates = Enum.reject(error_templates, &is_nil/1) underlying_errors = collect_deepest_underlying_errors(error_templates) precond_errors = diff --git a/lib/domo/type_ensurer_factory/generator/match_fun_registry.ex b/lib/domo/type_ensurer_factory/generator/match_fun_registry.ex index 3acd09a4..16a85e20 100644 --- a/lib/domo/type_ensurer_factory/generator/match_fun_registry.ex +++ b/lib/domo/type_ensurer_factory/generator/match_fun_registry.ex @@ -4,6 +4,7 @@ defmodule Domo.TypeEnsurerFactory.Generator.MatchFunRegistry do alias Domo.TypeEnsurerFactory.Generator.MatchFunRegistry.{ Lists, Literals, + OrElements, Tuples, Maps, Structs @@ -63,6 +64,7 @@ defmodule Domo.TypeEnsurerFactory.Generator.MatchFunRegistry do else {match_fun, underlying_type_spec_preconds} = cond do + OrElements.or_element_spec?(type_spec_precond) -> OrElements.match_spec_function_quoted(type_spec_precond) Lists.list_spec?(type_spec_precond) -> Lists.match_spec_function_quoted(type_spec_precond) Tuples.tuple_spec?(type_spec_precond) -> Tuples.match_spec_function_quoted(type_spec_precond) Maps.map_spec?(type_spec_precond) -> Maps.match_spec_function_quoted(type_spec_precond) diff --git a/lib/domo/type_ensurer_factory/generator/match_fun_registry/lists.ex b/lib/domo/type_ensurer_factory/generator/match_fun_registry/lists.ex index 1ba491c6..da59accf 100644 --- a/lib/domo/type_ensurer_factory/generator/match_fun_registry/lists.ex +++ b/lib/domo/type_ensurer_factory/generator/match_fun_registry/lists.ex @@ -152,45 +152,6 @@ defmodule Domo.TypeEnsurerFactory.Generator.MatchFunRegistry.Lists do {[match_spec_quoted, match_list_elements_quoted], [element_spec_precond]} end - defp match_el_quoted(type_spec_atom, head_attributes, tail_attributes) do - {head_spec_atom, head_precond_atom, head_spec_string} = head_attributes - {tail_spec_atom, tail_precond_atom, tail_spec_string} = tail_attributes - - quote do - def do_match_list_elements(unquote(type_spec_atom), [head | tail], idx, opts) do - case do_match_spec({unquote(head_spec_atom), unquote(head_precond_atom)}, head, unquote(head_spec_string), opts) do - :ok -> - do_match_list_elements(unquote(type_spec_atom), tail, idx + 1, opts) - - {:error, element_value, messages} -> - {:element_error, - [ - {"The element at index %{idx} has value %{element_value} that is invalid.", [idx: idx, element_value: inspect(element_value)]} - | messages - ]} - end - end - - def do_match_list_elements(unquote(type_spec_atom), [], idx, _opts) do - {:proper, idx} - end - - def do_match_list_elements(unquote(type_spec_atom), tail, idx, opts) do - case do_match_spec({unquote(tail_spec_atom), unquote(tail_precond_atom)}, tail, unquote(tail_spec_string), opts) do - :ok -> - {:improper, idx} - - {:error, element_value, messages} -> - {:element_error, - [ - {"The tail element has value %{element_value} that is invalid.", [element_value: inspect(element_value)]} - | messages - ]} - end - end - end - end - defp match_list_function_quoted(:maybe_improper_list, type_spec, precond) do {:maybe_improper_list, [], [head_spec_precond, tail_spec_precond]} = type_spec type_spec_atom = TypeSpec.to_atom(type_spec) @@ -277,4 +238,43 @@ defmodule Domo.TypeEnsurerFactory.Generator.MatchFunRegistry.Lists do {[match_spec_quoted, match_list_elements_quoted], [head_spec_precond, tail_spec_precond]} end + + defp match_el_quoted(type_spec_atom, head_attributes, tail_attributes) do + {head_spec_atom, head_precond_atom, head_spec_string} = head_attributes + {tail_spec_atom, tail_precond_atom, tail_spec_string} = tail_attributes + + quote do + def do_match_list_elements(unquote(type_spec_atom), [head | tail], idx, opts) do + case do_match_spec({unquote(head_spec_atom), unquote(head_precond_atom)}, head, unquote(head_spec_string), opts) do + :ok -> + do_match_list_elements(unquote(type_spec_atom), tail, idx + 1, opts) + + {:error, element_value, messages} -> + {:element_error, + [ + {"The element at index %{idx} has value %{element_value} that is invalid.", [idx: idx, element_value: inspect(element_value)]} + | messages + ]} + end + end + + def do_match_list_elements(unquote(type_spec_atom), [], idx, _opts) do + {:proper, idx} + end + + def do_match_list_elements(unquote(type_spec_atom), tail, idx, opts) do + case do_match_spec({unquote(tail_spec_atom), unquote(tail_precond_atom)}, tail, unquote(tail_spec_string), opts) do + :ok -> + {:improper, idx} + + {:error, element_value, messages} -> + {:element_error, + [ + {"The tail element has value %{element_value} that is invalid.", [element_value: inspect(element_value)]} + | messages + ]} + end + end + end + end end diff --git a/lib/domo/type_ensurer_factory/generator/match_fun_registry/or_elements.ex b/lib/domo/type_ensurer_factory/generator/match_fun_registry/or_elements.ex new file mode 100644 index 00000000..3d6cd314 --- /dev/null +++ b/lib/domo/type_ensurer_factory/generator/match_fun_registry/or_elements.ex @@ -0,0 +1,70 @@ +defmodule Domo.TypeEnsurerFactory.Generator.MatchFunRegistry.OrElements do + @moduledoc false + + alias Domo.TypeEnsurerFactory.Precondition + alias Domo.TypeEnsurerFactory.Generator.TypeSpec + + def or_element_spec?(type_spec_precond) do + {type_spec, _precond} = TypeSpec.split_spec_precond(type_spec_precond) + match?({:|, _, [_ | _]}, type_spec) + end + + def match_spec_function_quoted(type_spec_precond) do + {type_spec, precond} = TypeSpec.split_spec_precond(type_spec_precond) + + {:|, _, elements_spec_precond} = type_spec + + type_spec_atom = TypeSpec.to_atom(type_spec) + precond_atom = if precond, do: Precondition.to_atom(precond) + spec_string_var = if precond, do: quote(do: spec_string), else: quote(do: _spec_string) + + [l_elem_spec_precond, r_elem_spec_precond] = elements_spec_precond + {l_elem_spec_atom, l_elem_precond_atom, l_elem_spec_string} = TypeSpec.match_spec_attributes(l_elem_spec_precond) + {r_elem_spec_atom, r_elem_precond_atom, r_elem_spec_string} = TypeSpec.match_spec_attributes(r_elem_spec_precond) + + match_spec_quoted = + quote do + def do_match_spec({unquote(type_spec_atom), unquote(precond_atom)}, value, unquote(spec_string_var), opts) do + reply1 = do_match_spec({unquote(l_elem_spec_atom), unquote(l_elem_precond_atom)}, value, unquote(l_elem_spec_string), opts) + + if :ok == reply1 do + unquote(Precondition.ok_or_precond_call_quoted(precond, quote(do: spec_string), quote(do: value))) + else + reply2 = do_match_spec({unquote(r_elem_spec_atom), unquote(r_elem_precond_atom)}, value, unquote(r_elem_spec_string), opts) + + case reply2 do + :ok -> + unquote(Precondition.ok_or_precond_call_quoted(precond, quote(do: spec_string), quote(do: value))) + + {:error, value, _messages} -> + # find which reply has precondition error and use that one, or no error at all + {:error, _value, messages1} = reply1 + {:error, _value, messages2} = reply2 + + # we join all errors together + messages = messages1 ++ messages2 + + # we add nil to the error list to make the error builder function to form + # a general message about mismatching | sum type + {:error, value, [nil | messages]} + end + end + end + end + + {[match_spec_quoted], elements_spec_precond} + end + + def map_value_type(type_spec_precond, fun) do + {type_spec, precond} = TypeSpec.split_spec_precond(type_spec_precond) + + {:|, _, elements_spec_precond} = type_spec + + elements_spec_precond = + Enum.map(elements_spec_precond, fn case_spec_precond -> + fun.(case_spec_precond) + end) + + {{:|, [], elements_spec_precond}, precond} + end +end diff --git a/lib/domo/type_ensurer_factory/generator/type_spec.ex b/lib/domo/type_ensurer_factory/generator/type_spec.ex index 73b0ed52..e5e33dd5 100644 --- a/lib/domo/type_ensurer_factory/generator/type_spec.ex +++ b/lib/domo/type_ensurer_factory/generator/type_spec.ex @@ -6,6 +6,7 @@ defmodule Domo.TypeEnsurerFactory.Generator.TypeSpec do alias Domo.TypeEnsurerFactory.Generator.MatchFunRegistry.{ Lists, + OrElements, Tuples, Maps } @@ -40,6 +41,9 @@ defmodule Domo.TypeEnsurerFactory.Generator.TypeSpec do def filter_preconds(type_spec_precond) do result = cond do + OrElements.or_element_spec?(type_spec_precond) -> + OrElements.map_value_type(type_spec_precond, &filter_preconds/1) + Lists.list_spec?(type_spec_precond) -> Lists.map_value_type(type_spec_precond, &filter_preconds/1) diff --git a/lib/domo/type_ensurer_factory/resolver/fields.ex b/lib/domo/type_ensurer_factory/resolver/fields.ex index d9762fd8..a37b66f0 100644 --- a/lib/domo/type_ensurer_factory/resolver/fields.ex +++ b/lib/domo/type_ensurer_factory/resolver/fields.ex @@ -3,12 +3,9 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do alias Domo.TypeEnsurerFactory.Precondition alias Domo.TypeEnsurerFactory.Alias - alias Domo.TypeEnsurerFactory.Resolver.Fields.Arguments alias Domo.TypeEnsurerFactory.ModuleInspector alias Domo.TermSerializer - @max_arg_combinations_count 4096 - def resolve(mfe, preconds, remote_types_as_any, resolvable_structs) do {module, fields, env} = mfe @@ -52,19 +49,30 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do # Literals - defp resolve_type({:|, _meta, [arg1, arg2]} = type, module, precond, resolving_context, {types, errs, deps} = acc) do + defp resolve_type({:|, _meta, [arg1, arg2]} = type, module, precond, resolving_context, {types, errs, deps}) do if is_nil(precond) do {res_types, res_errs, res_deps} = resolve_type( - arg2, + arg1, module, - precond, + nil, resolving_context, - resolve_type(arg1, module, nil, resolving_context, acc) + resolve_type(arg2, module, nil, resolving_context, {[], [], []}) ) - res_types = res_types |> Enum.find(res_types, &match?({:any, _, _}, &1)) |> List.wrap() - {res_types, res_errs, res_deps} + # reject other options for any() + res_types = + res_types + |> Enum.find(res_types, &match?({:any, _, _}, &1)) + |> List.wrap() + + or_type_precond = + case res_types do + [resolved_type1, resolved_type2] -> {quote(do: unquote(resolved_type1) | unquote(resolved_type2)), nil} + [any_type] -> any_type + end + + {[or_type_precond | types], res_errs ++ errs, res_deps ++ deps} else type_string = Macro.to_string(type) @@ -80,19 +88,15 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do end defp resolve_type([{:..., _meta, _arg}], module, precond, _env_preconds, {types, errs, deps}) do - joint_type = {quote(context: module, do: nonempty_list(any())), precond} + any_type = {:any, [], []} + joint_type = {quote(context: module, do: nonempty_list(unquote(any_type))), precond} {[joint_type | types], errs, deps} end - defp resolve_type([type, {:..., _meta2, _arg2}], module, precond, resolving_context, acc) do - combine_or_args( - [type], - module, - resolving_context, - & &1, - fn [type] -> {quote(context: module, do: nonempty_list(unquote(type))), precond} end, - acc - ) + defp resolve_type([type, {:..., _meta2, _arg2}], module, precond, resolving_context, {types, errs, deps}) do + {[el_type], el_errs, el_deps} = resolve_type(type, module, nil, resolving_context, {[], [], []}) + list_type_precond = {quote(context: module, do: nonempty_list(unquote(el_type))), precond} + {[list_type_precond | types], el_errs ++ errs, el_deps ++ deps} end # Ecto.Schema Types @@ -129,35 +133,6 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do do_resolve_ecto_schema(:many, many_type, type, module, precond, resolving_context, acc) end - defp do_resolve_ecto_schema(schema_kind, schema_type, type, module, precond, resolving_context, {types, errs, deps} = acc) do - type_to_resolve = - case schema_kind do - :one -> quote(do: unquote(type) | Ecto.Association.NotLoaded.t()) - :many -> quote(do: [unquote(type)] | Ecto.Association.NotLoaded.t()) - end - - if is_nil(precond) do - {types, errors, deps} = - resolve_type( - type_to_resolve, - module, - precond, - resolving_context, - acc - ) - - {types, errors, deps} - else - error = - {:error, - """ - Precondition for value of Ecto.Schema.#{Atom.to_string(schema_type)}(t) type is not allowed.\ - """} - - {types, [error | errs], deps} - end - end - # Remote Types defp resolve_type({{:., _, [rem_module, rem_type]}, _, _}, _module, precond, resolving_context, {types, errs, deps}) do @@ -211,9 +186,19 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do # Basic and Built-in Types - defp resolve_type({:boolean = kind, _meta, _args}, _module, precond, _env_preconds, {types, errs, deps}) do + defp resolve_type({:boolean = kind, _meta, _args}, module, precond, _env_preconds, {types, errs, deps}) do if is_nil(precond) do - {[true, false | types], errs, deps} + joint_type = + or_type_quoted( + [ + true, + false + ], + module, + [nil] + ) + + {[joint_type | types], errs, deps} else error = {:error, precondition_not_supported_message(kind)} {types, [error | errs], deps} @@ -222,13 +207,18 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do defp resolve_type({:identifier = kind, _meta, _args}, module, precond, _env_preconds, {types, errs, deps}) do if is_nil(precond) do - joint_types = [ - quote(context: module, do: reference()), - quote(context: module, do: port()), - quote(context: module, do: pid()) - ] + joint_type = + or_type_quoted( + [ + quote(context: module, do: pid()), + quote(context: module, do: reference()), + quote(context: module, do: port()) + ], + module, + [nil, nil] + ) - {joint_types ++ types, errs, deps} + {[joint_type | types], errs, deps} else error = {:error, precondition_not_supported_message(kind)} {types, [error | errs], deps} @@ -266,22 +256,32 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do end defp resolve_type({:number, _meta, _args}, module, precond, _env_preconds, {types, errs, deps}) do - joint_types = [ - {quote(context: module, do: float()), precond}, - {quote(context: module, do: integer()), precond} - ] + joint_type = + or_type_quoted( + [ + {drop_line_metadata({:integer, [], []}), nil}, + {drop_line_metadata({:float, [], []}), nil} + ], + module, + [precond] + ) - {joint_types ++ types, errs, deps} + {[joint_type | types], errs, deps} end defp resolve_type({:timeout = kind, _meta, _args}, module, precond, _env_preconds, {types, errs, deps}) do if is_nil(precond) do - type_no_preconds = [ - {quote(context: module, do: non_neg_integer()), nil}, - quote(context: module, do: :infinity) - ] + joint_type = + or_type_quoted( + [ + :infinity, + {quote(context: module, do: non_neg_integer()), nil} + ], + module, + [nil] + ) - {type_no_preconds ++ types, errs, deps} + {[joint_type | types], errs, deps} else error = {:error, precondition_not_supported_message(kind)} {types, [error | errs], deps} @@ -326,19 +326,21 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do {[joint_type | types], errs, deps} end - defp resolve_type({:list, _meta, []}, module, precond, _env_preconds, {types, errs, deps}) do - joint_type = {quote(context: module, do: [any()]), precond} + defp resolve_type({:list, _meta, []}, _module, precond, _env_preconds, {types, errs, deps}) do + joint_type = {[{:any, [], []}], precond} {[joint_type | types], errs, deps} end defp resolve_type({:nonempty_list, _meta, []}, module, precond, _env_preconds, {types, errs, deps}) do - joint_type = {quote(context: module, do: nonempty_list(any())), precond} + any_type_precond = {:any, [], []} + joint_type = {quote(context: module, do: nonempty_list(unquote(any_type_precond))), precond} {[joint_type | types], errs, deps} end defp resolve_type({maybe_list_kind, _meta, []}, module, precond, _env_preconds, {types, errs, deps}) when maybe_list_kind in [:maybe_improper_list, :nonempty_maybe_improper_list] do - joint_type = {quote(context: module, do: unquote(maybe_list_kind)(any(), any())), precond} + any = {:any, [], []} + joint_type = {quote(context: module, do: unquote(maybe_list_kind)(unquote(any), unquote(any))), precond} {[joint_type | types], errs, deps} end @@ -355,9 +357,10 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do defp resolve_type({:struct, _meta, _args}, module, precond, _env_preconds, {types, errs, deps}) do struct_attribute = ModuleInspector.struct_attribute() + any = {:any, [], []} joint_type = { - quote(context: module, do: %{unquote(struct_attribute) => {atom(), nil}, optional({atom(), nil}) => any()}), + quote(context: module, do: %{unquote(struct_attribute) => {atom(), nil}, optional({atom(), nil}) => unquote(any)}), precond } @@ -378,20 +381,19 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do # Parametrized literals, basic, and built-in types defp resolve_type({:keyword, _meta, []}, module, precond, _env_preconds, {types, errs, deps}) do - joint_type = {quote(context: module, do: [{atom(), any()}]), precond} + atom_precond = {{:atom, [], []}, nil} + any = {:any, [], []} + joint_type = {quote(context: module, do: [{unquote(atom_precond), unquote(any)}]), precond} {[joint_type | types], errs, deps} end - defp resolve_type({:keyword, _meta, [type]}, module, precond, resolving_context, {types, errs, deps} = acc) do + defp resolve_type({:keyword, _meta, [type]}, module, precond, resolving_context, {types, errs, deps}) do if is_nil(precond) do - combine_or_args( - [type], - module, - resolving_context, - & &1, - fn [type] -> {quote(context: module, do: [{atom(), unquote(type)}]), nil} end, - acc - ) + {[el_type], el_errs, el_deps} = resolve_type(type, module, nil, resolving_context, {[], [], []}) + + atom_precond = {{:atom, [], []}, nil} + kw_type = quote(context: module, do: [{unquote(atom_precond), unquote(el_type)}]) + {[{kw_type, precond} | types], el_errs ++ errs, el_deps ++ deps} else error = {:error, @@ -405,37 +407,18 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do end defp resolve_type({:list, _meta, [arg]}, module, precond, resolving_context, acc) do - combine_or_args( - [arg], - module, - resolving_context, - & &1, - &{quote(context: module, do: unquote(&1)), precond}, - acc - ) - end - - defp resolve_type({:nonempty_list, _meta, [arg]}, module, precond, resolving_context, acc) do - combine_or_args( - [arg], - module, - resolving_context, - & &1, - fn [arg] -> {quote(context: module, do: nonempty_list(unquote(arg))), precond} end, - acc - ) + resolve_type([arg], module, precond, resolving_context, acc) + end + + defp resolve_type({:nonempty_list, _meta, [arg]}, module, precond, resolving_context, {types, errs, deps}) do + {[el_type], el_err, el_deps} = resolve_type(arg, module, nil, resolving_context, {[], [], []}) + list_type_precond = {quote(context: module, do: nonempty_list(unquote(el_type))), precond} + {[list_type_precond | types], el_err ++ errs, el_deps ++ deps} end defp resolve_type({:as_boolean, _meta, [type]}, module, precond, resolving_context, {types, errs, deps} = acc) do if is_nil(precond) do - combine_or_args( - [type], - module, - resolving_context, - & &1, - fn [type] -> quote(context: module, do: unquote(type)) end, - acc - ) + resolve_type(type, module, nil, resolving_context, acc) else error = {:error, @@ -458,28 +441,11 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do {[joint_type | types], errs, deps} end - defp resolve_type([{:->, _meta, [[_ | _] = args, _return_type]}], module, precond, resolving_context, acc) do - combine_or_args( - args, - module, - resolving_context, - & &1, - &{quote(context: module, do: (unquote_splicing(&1) -> any())), precond}, - acc - ) - end - - defp resolve_type({:%{}, _meta, [{{kind, _km, [key_type]}, value_type}]}, module, precond, resolving_context, acc) do - combine_or_args( - [key_type, value_type], - module, - resolving_context, - & &1, - fn [key_type, value_type] -> - {quote(context: module, do: %{unquote(kind)(unquote(key_type)) => unquote(value_type)}), precond} - end, - acc - ) + defp resolve_type([{:->, _meta, [[_ | _] = args, _return_type]}], module, precond, resolving_context, {types, errs, deps}) do + {el_types, el_errs, el_deps} = parallel_resolve_type(Enum.reverse(args), module, nil, resolving_context, {[], [], []}) + any = {:any, [], []} + fun_type_precond = {quote(context: module, do: (unquote_splicing(el_types) -> unquote(any))), precond} + {[fun_type_precond | types], el_errs ++ errs, el_deps ++ deps} end defp resolve_type({:%{}, _meta, [{{kind, _, [_key]}, _value} | _] = kkv}, module, precond, resolving_context, {types, errs, deps}) @@ -504,15 +470,19 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do {joint_types ++ types, resolved_errs ++ errs, deps ++ resolved_deps} end - defp resolve_type({:%{}, _meta, [{_key, _value} | _] = kv_list}, module, precond, resolving_context, acc) do - combine_or_args( - kv_list, - module, - resolving_context, - &drop_kv_precond/1, - &{quote(context: module, do: %{unquote_splicing(&1)}), precond}, - acc - ) + defp resolve_type({:%{}, _meta, [{_key, _value} | _] = kv_list}, module, precond, resolving_context, {types, errs, deps}) do + {el_types, el_errs, el_deps} = + kv_list + |> Enum.reverse() + |> Enum.reduce({[], [], []}, fn {key, value}, {types, errs, deps} -> + {[key_type], key_errs, key_deps} = resolve_type(key, module, nil, resolving_context, {[], [], []}) + {[value_type], value_errs, value_deps} = resolve_type(value, module, nil, resolving_context, {[], [], []}) + + {[{key_type, value_type} | types], key_errs ++ value_errs ++ errs, key_deps ++ value_deps ++ deps} + end) + + map_type_precond = {{:%{}, [], el_types}, precond} + {[map_type_precond | types], el_errs ++ errs, el_deps ++ deps} end defp resolve_type( @@ -562,18 +532,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do end end - defp resolve_type([_] = args, module, precond, resolving_context, acc) do - combine_or_args( - args, - module, - resolving_context, - &drop_kv_precond/1, - &{quote(context: module, do: [unquote_splicing(&1)]), precond}, - acc - ) - end - - defp resolve_type([_ | _] = list, module, precond, resolving_context, {types, errs, deps} = acc) do + defp resolve_type([{key, _value} | _] = list, module, precond, resolving_context, {types, errs, deps}) when is_atom(key) do keyword? = Enum.all?(list, fn {key, _value} -> is_atom(key) @@ -581,41 +540,38 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do end) if keyword? do - combine_or_args( - list, - module, - resolving_context, - &drop_kv_precond/1, - &{quote(context: module, do: [unquote_splicing(&1)]), precond}, - acc - ) + {keys, value_types} = Enum.unzip(list) + {resolved_value_types, resolved_errs, resolved_deps} = parallel_resolve_type(value_types, module, nil, resolving_context, {[], [], []}) + kv_types = Enum.zip(keys, resolved_value_types) + + {[{kv_types, precond} | types], resolved_errs ++ errs, resolved_deps ++ deps} else {types, [:keyword_list_should_has_atom_keys | errs], deps} end end - defp resolve_type({list_kind, _meta, [_elem_type, _tail_type] = el_types}, module, precond, resolving_context, acc) + defp resolve_type([el], module, precond, resolving_context, {types, errs, deps}) do + {[el_type], el_errs, el_deps} = resolve_type(el, module, nil, resolving_context, {[], [], []}) + {[{[el_type], precond} | types], el_errs ++ errs, el_deps ++ deps} + end + + defp resolve_type({list_kind, _meta, [_head_type, _tail_type] = ht_types}, module, precond, resolving_context, {types, errs, deps}) when list_kind in [ :maybe_improper_list, :nonempty_improper_list, :nonempty_maybe_improper_list ] do - combine_or_args( - el_types, - module, - resolving_context, - & &1, - fn [elem_type, tail_type] -> - { - quote( - context: module, - do: unquote(list_kind)(unquote(elem_type), unquote(tail_type)) - ), - precond - } - end, - acc - ) + {[head_type, tail_type], el_errs, el_deps} = parallel_resolve_type(Enum.reverse(ht_types), module, nil, resolving_context, {[], [], []}) + + list_type_precond = { + quote( + context: module, + do: unquote(list_kind)(unquote(head_type), unquote(tail_type)) + ), + precond + } + + {[list_type_precond | types], el_errs ++ errs, el_deps ++ deps} end defp resolve_type({:{} = kind, _meta, []}, module, precond, _env_preconds, {types, errs, deps}) do @@ -628,30 +584,21 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do end end - defp resolve_type({:{}, _meta, [_ | _] = args}, module, precond, resolving_context, acc) do - combine_or_args( - args, - module, - resolving_context, - & &1, - &{quote(context: module, do: {unquote_splicing(&1)}), precond}, - acc - ) + defp resolve_type({:{}, _meta, [_ | _] = args}, module, precond, resolving_context, {types, errs, deps}) do + {el_types, el_errs, el_deps} = parallel_resolve_type(Enum.reverse(args), module, nil, resolving_context, {[], [], []}) + tuple_type_precond = {quote(context: module, do: {unquote_splicing(el_types)}), precond} + {[tuple_type_precond | types], el_errs ++ errs, el_deps ++ deps} end - defp resolve_type({arg1, arg2}, module, precond, resolving_context, acc) do - combine_or_args( - [arg1, arg2], - module, - resolving_context, - & &1, - fn [arg1, arg2] -> {quote(context: module, do: {unquote(arg1), unquote(arg2)}), precond} end, - acc - ) + defp resolve_type({arg1, arg2}, module, precond, resolving_context, {types, errs, deps}) do + {[arg1_type, arg2_type], el_errs, el_deps} = parallel_resolve_type([arg2, arg1], module, nil, resolving_context, {[], [], []}) + tuple_type_precond = {quote(context: module, do: {unquote(arg1_type), unquote(arg2_type)}), precond} + {[tuple_type_precond | types], el_errs ++ errs, el_deps ++ deps} end defp resolve_type({kind_any, _meta, args}, _module, precond, _env_preconds, {_types, errs, deps}) when kind_any in [:term, :any] do + # we use this hack because for any as a type parameter and any from an external field type with precondition type = if is_nil(precond), do: {:any, [], args}, else: {{:any, [], args}, precond} {[type], errs, deps} end @@ -773,45 +720,56 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do {[joint_type | types], errs, deps} end - defp combine_or_args(args, module, resolving_context, map_resolved_fn, quote_fn, {types, errs, deps}) do - {args_resolved, errs_resolved, deps_resolved} = - args - |> Enum.map(&resolve_type(&1, module, nil, resolving_context, {[], [], []})) - |> Enum.reduce({[], [], []}, fn {args_el, errs_el, deps_el}, {args_resolved, errs_resolved, deps_resolved} -> - {[args_el | args_resolved], [errs_el | errs_resolved], [deps_el | deps_resolved]} - end) - - {args_resolved, errs_resolved, deps_resolved} = { - Enum.reverse(args_resolved) |> Enum.map(&map_resolved_fn.(&1)), - Enum.reverse(errs_resolved), - Enum.reverse(deps_resolved) - } + defp do_resolve_ecto_schema(schema_kind, schema_type, type, module, precond, resolving_context, {types, errs, deps} = acc) do + type_to_resolve = + case schema_kind do + :one -> quote(do: unquote(type) | Ecto.Association.NotLoaded.t()) + :many -> quote(do: [unquote(type)] | Ecto.Association.NotLoaded.t()) + end - args_combinations_count = Enum.reduce(args_resolved, 1, fn sublist, acc -> Enum.count(sublist) * acc end) + if is_nil(precond) do + {types, errors, deps} = + resolve_type( + type_to_resolve, + module, + precond, + resolving_context, + acc + ) - if args_combinations_count > @max_arg_combinations_count do - err = + {types, errors, deps} + else + error = {:error, """ - Failed to generate #{args_combinations_count} type combinations with max. allowed #{@max_arg_combinations_count}. \ - Consider reducing number of | options or change the container type to struct using Domo.\ + Precondition for value of Ecto.Schema.#{Atom.to_string(schema_type)}(t) type is not allowed.\ """} - {types, [err | errs], deps} - else - combined_types = - args_resolved - |> Arguments.all_combinations() - |> Enum.map("e_fn.(&1)) - - { - combined_types ++ types, - List.flatten(errs_resolved) ++ errs, - deps ++ List.flatten(deps_resolved) - } + {types, [error | errs], deps} end end + # this one is for internal cases when we have several arguments to be resolved, f.e. from {a, b, c} + # it resolves each element independently so f.e. :any as one resolved element doesn't affect others + defp parallel_resolve_type([_ | _] = list, module, nil, resolving_context, acc) do + Enum.reduce(list, acc, fn type, {types, errs, deps} -> + {el_type, el_errs, el_deps} = resolve_type(type, module, nil, resolving_context, {[], [], []}) + {el_type ++ types, el_errs ++ errs, el_deps ++ deps} + end) + end + + defp or_type_quoted(types, module, preconds) when length(preconds) == length(types) - 1 do + do_or_type_quoted(types, module, preconds) + end + + defp do_or_type_quoted([type_head], module, []) do + quote(context: module, do: unquote(type_head)) + end + + defp do_or_type_quoted([type_head | type_tail], module, [precond_head | precond_tail]) do + quote(context: module, do: {unquote(type_head) | unquote(or_type_quoted(type_tail, module, precond_tail)), unquote(precond_head)}) + end + defp get_valid_precondition(preconditions) do preconditions = Enum.reject(preconditions, &is_nil/1) @@ -838,13 +796,6 @@ defmodule Domo.TypeEnsurerFactory.Resolver.Fields do defp drop_line_metadata(type), do: Macro.update_meta(type, &Keyword.delete(&1, :line)) - defp drop_kv_precond(kv_list) do - Enum.map(kv_list, fn - {{_key, _value} = kv, _precond} -> kv - value -> value - end) - end - defp ensurable_struct?(module, resolvable_structs) do MapSet.member?(resolvable_structs, module) or ModuleInspector.has_type_ensurer?(module) end diff --git a/mix.exs b/mix.exs index ef4f8030..01607158 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Domo.MixProject do use Mix.Project - @version "1.5.14" + @version "1.5.15" @repo_url "https://github.com/IvanRublev/Domo" def project do @@ -69,9 +69,10 @@ defmodule Domo.MixProject do {:placebo, "~> 1.2", only: :test}, {:ecto, ">= 0.0.0", optional: true}, {:decimal, ">= 0.0.0", optional: true}, + {:nimble_parsec, "1.1.0"}, # Documentation dependencies - {:ex_doc, ">= 0.0.0", only: :docs, runtime: false} + {:ex_doc, "0.26.0", only: :docs, runtime: false} ] end diff --git a/test/domo/changeset_test.exs b/test/domo/changeset_test.exs index eeb8878e..13546cf8 100644 --- a/test/domo/changeset_test.exs +++ b/test/domo/changeset_test.exs @@ -37,7 +37,7 @@ defmodule Domo.ChangesetTest do Changeset.validate_type(changeset, trim: true) - assert_receive {:validate_required_was_called, ^changeset, [:subtitle, :title], [trim: true]} + assert_receive {:validate_required_was_called, ^changeset, [:age, :subtitle, :title], [trim: true]} end test "does not call validate_required/2 with Ecto.Schema typed fields of @t" do @@ -74,6 +74,10 @@ defmodule Domo.ChangesetTest do test "validates each given field of struct by type ensurer in call to Ecto.Changeset.validate_change/3" do me = self() + allow Ecto.Changeset.validate_required(any(), any(), any()), + meck_options: [:passthrough], + exec: fn changeset, _fields, _opts -> changeset end + allow Ecto.Changeset.validate_change(any(), any(), any()), meck_options: [:passthrough], exec: fn changeset, field, fun -> @@ -100,6 +104,10 @@ defmodule Domo.ChangesetTest do test "ignores __meta__ Ecto field in calling to Ecto.Changeset.validate_change/3" do me = self() + allow Ecto.Changeset.validate_required(any(), any(), any()), + meck_options: [:passthrough], + exec: fn changeset, _fields, _opts -> changeset end + allow Ecto.Changeset.validate_change(any(), any(), any()), meck_options: [:passthrough], exec: fn changeset, field, fun -> @@ -215,6 +223,10 @@ defmodule Domo.ChangesetTest do test "returns empty list for ok or error list back to Ecto.Changeset.validate_change/3 call on validation" do me = self() + allow Ecto.Changeset.validate_required(any(), any(), any()), + meck_options: [:passthrough], + exec: fn changeset, _fields, _opts -> changeset end + allow Ecto.Changeset.validate_change(any(), any(), any()), meck_options: [:passthrough], exec: fn changeset, field, fun -> @@ -237,7 +249,10 @@ defmodule Domo.ChangesetTest do [ title: """ Invalid value :hello for field :title of %CustomStructUsingDomo{}. \ - Expected the value matching the <<_::_*8>> | nil type.\ + Expected the value matching the <<_::_*8>> | nil type. + Underlying errors: + - Expected the value matching the <<_::_*8>> type. + - Expected the value matching the nil type.\ """ ]} end @@ -367,6 +382,10 @@ defmodule Domo.ChangesetTest do test "validates each field of struct by type ensurer in call to Ecto.Changeset.validate_change/3" do me = self() + allow Ecto.Changeset.validate_required(any(), any(), any()), + meck_options: [:passthrough], + exec: fn changeset, _fields, _opts -> changeset end + allow Ecto.Changeset.validate_change(any(), any(), any()), meck_options: [:passthrough], exec: fn changeset, field, fun -> @@ -393,6 +412,10 @@ defmodule Domo.ChangesetTest do test "returns empty list for ok or error list back to Ecto.Changeset.validate_change/3 call on validation" do me = self() + allow Ecto.Changeset.validate_required(any(), any(), any()), + meck_options: [:passthrough], + exec: fn changeset, _fields, _opts -> changeset end + allow Ecto.Changeset.validate_change(any(), any(), any()), meck_options: [:passthrough], exec: fn changeset, field, fun -> @@ -415,7 +438,10 @@ defmodule Domo.ChangesetTest do [ title: """ Invalid value :hello for field :title of %CustomStructUsingDomo{}. \ - Expected the value matching the <<_::_*8>> | nil type.\ + Expected the value matching the <<_::_*8>> | nil type. + Underlying errors: + - Expected the value matching the <<_::_*8>> type. + - Expected the value matching the nil type.\ """ ]} end @@ -432,6 +458,10 @@ defmodule Domo.ChangesetTest do test "validates each given field by type ensurer in call to Ecto.Changeset.validate_change/3" do me = self() + allow Ecto.Changeset.validate_required(any(), any(), any()), + meck_options: [:passthrough], + exec: fn changeset, _fields, _opts -> changeset end + allow Ecto.Changeset.validate_change(any(), any(), any()), meck_options: [:passthrough], exec: fn changeset, field, fun -> diff --git a/test/domo/type_ensurer_factory/generator_test.exs b/test/domo/type_ensurer_factory/generator_test.exs index 4288f1df..dc500e0c 100644 --- a/test/domo/type_ensurer_factory/generator_test.exs +++ b/test/domo/type_ensurer_factory/generator_test.exs @@ -42,7 +42,12 @@ defmodule Domo.TypeEnsurerFactory.GeneratorTest do end describe "generate/2" do - test "makes code directory", %{types_file: types_file, ecto_assocs_file: ecto_assocs_file, t_reflections_file: t_reflections_file, code_path: code_path} do + test "makes code directory", %{ + types_file: types_file, + ecto_assocs_file: ecto_assocs_file, + t_reflections_file: t_reflections_file, + code_path: code_path + } do File.rm_rf(code_path) Generator.generate(types_file, ecto_assocs_file, t_reflections_file, code_path) @@ -67,11 +72,12 @@ defmodule Domo.TypeEnsurerFactory.GeneratorTest do } = Generator.generate(types_file, ecto_assocs_file, t_reflections_file, code_path, FailingMkdirFile) end - @tag types_content: types_by_module_content(%{ - Module => %{first: [quote(do: integer())], second: [quote(do: float())]}, - Some.Nested.Module1 => %{former: [quote(do: integer())]}, - EmptyStruct => %{} - }) + @tag types_content: + types_by_module_content(%{ + Module => %{first: [quote(do: integer())], second: [quote(do: float())]}, + Some.Nested.Module1 => %{former: [quote(do: integer())]}, + EmptyStruct => %{} + }) test "writes TypeEnsurer source code to code_path for each module from types file", %{ types_file: types_file, ecto_assocs_file: ecto_assocs_file, @@ -93,11 +99,12 @@ defmodule Domo.TypeEnsurerFactory.GeneratorTest do assert size2 > 0 end - @tag types_content: types_by_module_content(%{ - Module => %{first: [quote(do: integer())], second: [quote(do: float())]}, - Some.Nested.Module1 => %{former: [quote(do: integer())]}, - EmptyStruct => %{} - }) + @tag types_content: + types_by_module_content(%{ + Module => %{first: [quote(do: integer())], second: [quote(do: float())]}, + Some.Nested.Module1 => %{former: [quote(do: integer())]}, + EmptyStruct => %{} + }) test "returns list of TypeEnsurer modules source code file paths", %{ types_file: types_file, ecto_assocs_file: ecto_assocs_file, @@ -109,11 +116,12 @@ defmodule Domo.TypeEnsurerFactory.GeneratorTest do type_ensurer2_path = Path.join(code_path, "/empty_struct_type_ensurer.ex") assert {:ok, list} = Generator.generate(types_file, ecto_assocs_file, t_reflections_file, code_path) + assert [ - type_ensurer2_path, - type_ensurer_path, - type_ensurer1_path - ] == Enum.sort(list) + type_ensurer2_path, + type_ensurer_path, + type_ensurer1_path + ] == Enum.sort(list) end @tag types_content: nil @@ -133,11 +141,12 @@ defmodule Domo.TypeEnsurerFactory.GeneratorTest do } = Generator.generate(types_file, ecto_assocs_file, t_reflections_file, code_path) end - @tag types_content: types_by_module_content(%{ - Module => %{first: [quote(do: integer())], second: [quote(do: float())]}, - Some.Nested.Module1 => %{former: [quote(do: integer())]}, - EmptyStruct => %{} - }) + @tag types_content: + types_by_module_content(%{ + Module => %{first: [quote(do: integer())], second: [quote(do: float())]}, + Some.Nested.Module1 => %{former: [quote(do: integer())]}, + EmptyStruct => %{} + }) @tag ecto_assocs_content: nil test "returns error if read of ecto assocs file failed", %{ types_file: types_file, @@ -216,9 +225,11 @@ defmodule Domo.TypeEnsurerFactory.GeneratorTest do def read(_path) do {:ok, - TermSerializer.term_to_binary(types_by_module_content(%{ - Some.Nested.Module1 => %{former: [quote(do: integer())]} - }))} + TermSerializer.term_to_binary( + types_by_module_content(%{ + Some.Nested.Module1 => %{former: [quote(do: integer())]} + }) + )} end def write(_path, _content), do: {:error, :eaccess} diff --git a/test/domo/type_ensurer_factory/generator_type_ensurer_module_test.exs b/test/domo/type_ensurer_factory/generator_type_ensurer_module_test.exs index 86dc958f..bb4deb41 100644 --- a/test/domo/type_ensurer_factory/generator_type_ensurer_module_test.exs +++ b/test/domo/type_ensurer_factory/generator_type_ensurer_module_test.exs @@ -41,16 +41,20 @@ defmodule Domo.TypeEnsurerFactory.GeneratorTypeEnsurerModuleTest do describe "Generated TypeEnsurer module" do setup do - load_type_ensurer_module_with_no_preconds(%{ - __example_meta_field__: [quote(do: atom())], - __any_meta_field__: [quote(do: term())], - second: [quote(do: integer())], - first: [quote(do: integer()), quote(do: nil)], - third: [quote(do: any())], - fourth: [quote(do: term())], - ecto_assoc_1: [quote(do: [atom()])], - ecto_assoc_2: [quote(do: atom())], - }, [:ecto_assoc_1, :ecto_assoc_2], "%Module{...}") + load_type_ensurer_module_with_no_preconds( + %{ + __example_meta_field__: [quote(do: atom())], + __any_meta_field__: [quote(do: term())], + second: [quote(do: integer())], + first: [quote(do: integer()), quote(do: nil)], + third: [quote(do: any())], + fourth: [quote(do: term())], + ecto_assoc_1: [quote(do: [atom()])], + ecto_assoc_2: [quote(do: atom())] + }, + [:ecto_assoc_1, :ecto_assoc_2], + "%Module{...}" + ) :ok end @@ -72,7 +76,16 @@ defmodule Domo.TypeEnsurerFactory.GeneratorTypeEnsurerModuleTest do end test "fields(:typed_with_meta_with_any) returns fields sorted alphabetically with specific types with __meta fields__ included" do - assert call_fields(:typed_with_meta_with_any) == [:__any_meta_field__, :__example_meta_field__, :ecto_assoc_1, :ecto_assoc_2, :first, :fourth, :second, :third] + assert call_fields(:typed_with_meta_with_any) == [ + :__any_meta_field__, + :__example_meta_field__, + :ecto_assoc_1, + :ecto_assoc_2, + :first, + :fourth, + :second, + :third + ] end test "fields(:required) returns fields having no any or nil type sorted alphabetically with __meta fields__ rejected" do @@ -652,14 +665,14 @@ defmodule Domo.TypeEnsurerFactory.GeneratorTypeEnsurerModuleTest do test "port" do load_type_ensurer_module_with_no_preconds(%{first: [quote(do: port())]}) - assert :ok == call_ensure_field_type({:first, :erlang.list_to_port('#Port<0.0>')}) + assert :ok == call_ensure_field_type({:first, :erlang.list_to_port(~c"#Port<0.0>")}) assert {:error, _} = call_ensure_field_type({:first, :not_a_port}) end test "reference" do load_type_ensurer_module_with_no_preconds(%{first: [quote(do: reference())]}) - assert :ok == call_ensure_field_type({:first, :erlang.list_to_ref('#Ref<0.0.0.0>')}) + assert :ok == call_ensure_field_type({:first, :erlang.list_to_ref(~c"#Ref<0.0.0.0>")}) assert {:error, _} = call_ensure_field_type({:first, :not_a_ref}) end @@ -813,7 +826,8 @@ defmodule Domo.TypeEnsurerFactory.GeneratorTypeEnsurerModuleTest do test "function" do load_type_ensurer_module_with_no_preconds(%{ - first: [quote(do: (() -> any()))], + # quote(do: (() -> any())) + first: [[{:->, [], [[], {:any, [], []}]}]], second: [quote(do: (... -> any()))] }) @@ -913,7 +927,7 @@ defmodule Domo.TypeEnsurerFactory.GeneratorTypeEnsurerModuleTest do end end - describe "Generated TypeEnsurer module verifies basic/listeral typed" do + describe "Generated TypeEnsurer module verifies basic/literal typed" do test "proper [t]" do load_type_ensurer_module_with_no_preconds(%{ first: [quote(do: [atom()])] @@ -929,6 +943,20 @@ defmodule Domo.TypeEnsurerFactory.GeneratorTypeEnsurerModuleTest do assert {:error, _} = call_ensure_field_type({:first, :not_a_list}) end + test "proper [a | b]" do + load_type_ensurer_module_with_no_preconds(%{ + first: [quote(do: [atom() | integer()])] + }) + + assert :ok == call_ensure_field_type({:first, []}) + assert :ok == call_ensure_field_type({:first, [:one]}) + assert :ok == call_ensure_field_type({:first, [1]}) + assert :ok == call_ensure_field_type({:first, [:one, :two, :three]}) + assert :ok == call_ensure_field_type({:first, [:one, 2, 3]}) + assert {:error, _} = call_ensure_field_type({:first, [:first, :second, "third", :forth]}) + assert {:error, _} = call_ensure_field_type({:first, :not_a_list}) + end + test "nonempty_list(t)" do load_type_ensurer_module_with_no_preconds(%{ first: [quote(do: nonempty_list(atom()))] @@ -1048,7 +1076,7 @@ defmodule Domo.TypeEnsurerFactory.GeneratorTypeEnsurerModuleTest do end end - describe "Generated TypeEnsurer module verifies basic/listeral typed tuples of" do + describe "Generated TypeEnsurer module verifies basic/literal typed tuples of" do test "one element" do load_type_ensurer_module_with_no_preconds(%{ first: [quote(do: {atom()})] @@ -1115,7 +1143,7 @@ defmodule Domo.TypeEnsurerFactory.GeneratorTypeEnsurerModuleTest do end end - describe "Generated TypeEnsurer module verifies basic/listeral typed maps of" do + describe "Generated TypeEnsurer module verifies basic/literal typed maps of" do test "given keys and value types" do load_type_ensurer_module_with_no_preconds(%{ first: [quote(do: %{former: atom()})], diff --git a/test/domo/type_ensurer_factory/module_inspector_test.exs b/test/domo/type_ensurer_factory/module_inspector_test.exs index bdc2b085..ea6588db 100644 --- a/test/domo/type_ensurer_factory/module_inspector_test.exs +++ b/test/domo/type_ensurer_factory/module_inspector_test.exs @@ -67,7 +67,7 @@ defmodule Domo.TypeEnsurerFactory.ModuleInspectorTest do allow ResolvePlanner.get_types(:in_memory, NonexistentModule), return: {:error, :no_types_registered} ModuleInspector.beam_types(NonexistentModule) - refute_called ResolvePlanner.get_types([:in_memory, NonexistentModule]) + refute_called(ResolvePlanner.get_types([:in_memory, NonexistentModule])) end end @@ -89,9 +89,13 @@ defmodule Domo.TypeEnsurerFactory.ModuleInspectorTest do test "return no_beam_file error if no beam file can be found for module or no types can be loaded" do allow Code.Typespec.fetch_types(any()), meck_options: [:passthrough], return: :error - allow ResolvePlanner.get_types(:in_memory, NonexistentModule), meck_options: [:passthrough], return: {:no_beam_file, ModuleNested.Module.Submodule} + + allow ResolvePlanner.get_types(:in_memory, NonexistentModule), + meck_options: [:passthrough], + return: {:no_beam_file, ModuleNested.Module.Submodule} + assert {:error, {:no_beam_file, ModuleNested.Module.Submodule}} == ModuleInspector.beam_types(ModuleNested.Module.Submodule) - refute_called ResolvePlanner.get_types(:in_memory, ModuleNested.Module.Submodule) + refute_called(ResolvePlanner.get_types(:in_memory, ModuleNested.Module.Submodule)) end test "return type_not_found error when can't find :t type in quoted types list" do @@ -102,14 +106,14 @@ defmodule Domo.TypeEnsurerFactory.ModuleInspectorTest do type_list = [ {:"::", [], [{:my_atom, [], []}, {:atom, [line: 1], []}]}, {:"::", [line: 11], - [ - {:t, [line: 11], nil}, - {:%, [line: 11], - [ - {:__MODULE__, [line: 11], nil}, - {:%{}, [line: 11], [title: {:title, [line: 11], []}]} - ]} - ]} + [ + {:t, [line: 11], nil}, + {:%, [line: 11], + [ + {:__MODULE__, [line: 11], nil}, + {:%{}, [line: 11], [title: {:title, [line: 11], []}]} + ]} + ]} ] assert {:ok, _, []} = ModuleInspector.find_t_type(type_list) @@ -130,8 +134,16 @@ defmodule Domo.TypeEnsurerFactory.ModuleInspectorTest do end test "return hash of module types giving loadable module" do - assert <<165, 63, 215, 58, 173, 14, 220, 157, 192, 81, 20, 19, 68, 90, 147, 171>> == - ModuleInspector.beam_types_hash(EmptyStruct) + module_hash = + case ElixirVersion.version() do + [1, minor, _] when minor < 16 -> + <<165, 63, 215, 58, 173, 14, 220, 157, 192, 81, 20, 19, 68, 90, 147, 171>> + + [1, minor, _] when minor >= 16 -> + <<210, 153, 0, 251, 50, 73, 13, 33, 58, 87, 29, 116, 203, 250, 237, 148>> + end + + assert module_hash == ModuleInspector.beam_types_hash(EmptyStruct) end test "return nil as hash of module types giving unloadable module" do @@ -156,7 +168,7 @@ defmodule Domo.TypeEnsurerFactory.ModuleInspectorTest do test "find remote Elixir type referenced by private local type and return it in the quoted form" do type_list = [ typep: {:rem_str, {:remote_type, 16, [{:atom, 0, String}, {:atom, 0, :t}, []]}, []}, - type: {:ut, {:user_type, 17, :rem_str, []}, ''} + type: {:ut, {:user_type, 17, :rem_str, []}, ~c""} ] assert {:ok, {{:., [], [String, :t]}, [], []}, [:rem_str]} == @@ -175,7 +187,7 @@ defmodule Domo.TypeEnsurerFactory.ModuleInspectorTest do test "find local user type in the list recursively and return it in quoted form" do type_list = [ {:typep, {:priv_atom, {:type, 16, :atom, []}, []}}, - {:type, {:ut, {:user_type, 17, :priv_atom, []}, ''}} + {:type, {:ut, {:user_type, 17, :priv_atom, []}, ~c""}} ] assert {:ok, quote(do: atom()), [:priv_atom]} == ModuleInspector.find_beam_type_quoted(:ut, type_list) @@ -201,7 +213,7 @@ defmodule Domo.TypeEnsurerFactory.ModuleInspectorTest do test "should resolve types from memory if in-memory resolve planner is started" do allow ResolvePlanner.started?(:in_memory), meck_options: [:passthrough], return: true - expect ResolvePlanner.get_types(:in_memory, NonexistentModule), meck_options: [:passthrough], return: {:error, :no_types_registered} + expect(ResolvePlanner.get_types(:in_memory, NonexistentModule), meck_options: [:passthrough], return: {:error, :no_types_registered}) TypeEnsurerFactory.start_resolve_planner(:in_memory, :in_memory, []) ModuleInspector.beam_types(NonexistentModule) @@ -221,7 +233,7 @@ defmodule Domo.TypeEnsurerFactory.ModuleInspectorTest do end test "return error not finding module in memory or in beam" do - expect ResolvePlanner.get_types(:in_memory, NonexistentModule), meck_options: [:passthrough], return: {:error, :no_types_registered} + expect(ResolvePlanner.get_types(:in_memory, NonexistentModule), meck_options: [:passthrough], return: {:error, :no_types_registered}) TypeEnsurerFactory.start_resolve_planner(:in_memory, :in_memory, []) assert {:error, :no_types_registered} == ModuleInspector.beam_types(NonexistentModule) end diff --git a/test/domo/type_ensurer_factory/resolve_planner_in_memory_test.exs b/test/domo/type_ensurer_factory/resolve_planner_in_memory_test.exs index 829f2ab5..30960507 100644 --- a/test/domo/type_ensurer_factory/resolve_planner_in_memory_test.exs +++ b/test/domo/type_ensurer_factory/resolve_planner_in_memory_test.exs @@ -117,8 +117,6 @@ defmodule Domo.TypeEnsurerFactory.ResolvePlannerInMemoryTest do ) end - - test "accept types to treat as any" do assert :ok == ResolvePlanner.keep_global_remote_types_to_treat_as_any( diff --git a/test/domo/type_ensurer_factory/resolver/basic_test.exs b/test/domo/type_ensurer_factory/resolver/basic_test.exs index 2b5c3728..8113d853 100644 --- a/test/domo/type_ensurer_factory/resolver/basic_test.exs +++ b/test/domo/type_ensurer_factory/resolver/basic_test.exs @@ -166,11 +166,11 @@ defmodule Domo.TypeEnsurerFactory.Resolver.BasicsTest do assert %{ TwoFieldStruct => - types_content_empty_precond(%{ + types_content_empty_precond(%{ first: [quote(do: integer())] }), AllDefaultsStruct => - types_content_empty_precond(%{ + types_content_empty_precond(%{ first: [quote(do: integer())], second: [quote(do: float())] }) diff --git a/test/domo/type_ensurer_factory/resolver/or_test.exs b/test/domo/type_ensurer_factory/resolver/or_test.exs index aa1120d7..f454b4e8 100644 --- a/test/domo/type_ensurer_factory/resolver/or_test.exs +++ b/test/domo/type_ensurer_factory/resolver/or_test.exs @@ -1,7 +1,6 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do use Domo.FileCase - alias Domo.TypeEnsurerFactory.Error alias Domo.TypeEnsurerFactory.Resolver import ResolverTestHelper @@ -9,7 +8,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do setup [:setup_project_planner] describe "TypeEnsurerFactory.Resolver should" do - test "resolve literals and basic t1 | t1 to list [t1, t1]", %{ + test "resolve literals and basic t1 and t2 in t1 | t2", %{ planner: planner, plan_file: plan_file, preconds_file: preconds_file, @@ -36,15 +35,14 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do for {arg1, arg2} <- Enum.zip(literals_and_basic_dst(), shift_list.(literals_and_basic_dst())) do [ - quote(context: TwoFieldStruct, do: unquote(arg1)), - quote(context: TwoFieldStruct, do: unquote(arg2)) + quote(context: TwoFieldStruct, do: unquote(arg1) | unquote(arg2)) ] end assert %{TwoFieldStruct => map_idx_list_multitype(expected)} == read_types(types_file) end - test "resolve multiple | to list", %{ + test "resolve operands of multiple |", %{ planner: planner, plan_file: plan_file, preconds_file: preconds_file, @@ -54,7 +52,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do t_reflections_file: t_reflections_file } do plan_types( - [quote(context: TwoFieldStruct, do: atom() | integer() | float() | list())], + [quote(context: TwoFieldStruct, do: module() | integer() | float() | list())], planner ) @@ -62,36 +60,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do expected = [ [ - quote(context: TwoFieldStruct, do: atom()), - quote(context: TwoFieldStruct, do: integer()), - quote(context: TwoFieldStruct, do: float()), - quote(context: TwoFieldStruct, do: [any()]) - ] - ] - - assert %{TwoFieldStruct => map_idx_list_multitype(expected)} == read_types(types_file) - end - - test "resolve | to list rejecting duplicates", %{ - planner: planner, - plan_file: plan_file, - preconds_file: preconds_file, - types_file: types_file, - deps_file: deps_file, - ecto_assocs_file: ecto_assocs_file, - t_reflections_file: t_reflections_file - } do - plan_types( - [quote(context: TwoFieldStruct, do: atom() | integer() | atom() | atom())], - planner - ) - - :ok = Resolver.resolve(plan_file, preconds_file, types_file, deps_file, ecto_assocs_file, t_reflections_file, false) - - expected = [ - [ - quote(context: TwoFieldStruct, do: atom()), - quote(context: TwoFieldStruct, do: integer()) + quote(context: TwoFieldStruct, do: atom() | integer() | float() | [any()]) ] ] @@ -127,7 +96,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do assert %{TwoFieldStruct => map_idx_list_multitype(expected)} == read_types(types_file) end - test "resolve tuple with multiple | arguments to list of tuples", %{ + test "resolve a and b in {a | b}", %{ planner: planner, plan_file: plan_file, preconds_file: preconds_file, @@ -140,7 +109,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do [ quote( context: TwoFieldStruct, - do: {atom() | integer(), float() | pid(), port() | atom()} + do: {module() | integer(), float() | pid(), port() | atom()} ) ], planner @@ -150,21 +119,17 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do expected = [ [ - quote(context: TwoFieldStruct, do: {atom(), float(), port()}), - quote(context: TwoFieldStruct, do: {atom(), float(), atom()}), - quote(context: TwoFieldStruct, do: {atom(), pid(), port()}), - quote(context: TwoFieldStruct, do: {atom(), pid(), atom()}), - quote(context: TwoFieldStruct, do: {integer(), float(), port()}), - quote(context: TwoFieldStruct, do: {integer(), float(), atom()}), - quote(context: TwoFieldStruct, do: {integer(), pid(), port()}), - quote(context: TwoFieldStruct, do: {integer(), pid(), atom()}) + quote( + context: TwoFieldStruct, + do: {atom() | integer(), float() | pid(), port() | atom()} + ) ] ] assert %{TwoFieldStruct => map_idx_list_multitype(expected)} == read_types(types_file) end - test "resolve | with nested tuples to list", %{ + test "resolve a and b with nested tuples {c, {a | b}}", %{ planner: planner, plan_file: plan_file, preconds_file: preconds_file, @@ -177,7 +142,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do [ quote( context: TwoFieldStruct, - do: 2 | {pid(), port(), atom() | {integer() | float(), 1}} + do: 2 | {pid(), port(), module() | {integer() | module(), 1}} ) ], planner @@ -187,17 +152,17 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do expected = [ [ - quote(context: TwoFieldStruct, do: 2), - quote(context: TwoFieldStruct, do: {pid(), port(), atom()}), - quote(context: TwoFieldStruct, do: {pid(), port(), {integer(), 1}}), - quote(context: TwoFieldStruct, do: {pid(), port(), {float(), 1}}) + quote( + context: TwoFieldStruct, + do: 2 | {pid(), port(), atom() | {integer() | atom(), 1}} + ) ] ] assert %{TwoFieldStruct => map_idx_list_multitype(expected)} == read_types(types_file) end - test "resolve | within [] to list", %{ + test "resolve a and b within [a | b]", %{ planner: planner, plan_file: plan_file, preconds_file: preconds_file, @@ -210,7 +175,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do [ quote( context: TwoFieldStruct, - do: 2 | [pid() | [integer() | float()]] + do: [integer() | module()] ) ], planner @@ -220,17 +185,14 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do expected = [ [ - quote(context: TwoFieldStruct, do: 2), - [quote(context: TwoFieldStruct, do: pid())], - [[quote(context: TwoFieldStruct, do: integer())]], - [[quote(context: TwoFieldStruct, do: float())]] + [quote(context: TwoFieldStruct, do: integer() | atom())] ] ] assert %{TwoFieldStruct => map_idx_list_multitype(expected)} == read_types(types_file) end - test "resolve | within proper and improper lists to list of lists", %{ + test "resolve a and b within proper and improper lists (a | b) ", %{ planner: planner, plan_file: plan_file, preconds_file: preconds_file, @@ -246,10 +208,10 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do do: list( nonempty_list( - nonempty_improper_list(1 | 2, 3 | 4) - | nonempty_maybe_improper_list(5 | 6, 7 | 8) + nonempty_improper_list(1 | module(), 3 | 4) + | nonempty_maybe_improper_list(5 | 6, 7 | module()) ) - | maybe_improper_list([9 | 10, ...], 11 | 12) + | maybe_improper_list([9 | module(), ...], module() | 12) ) ) ], @@ -260,25 +222,21 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do expected = [ [ - [quote(context: TwoFieldStruct, do: nonempty_list(nonempty_improper_list(1, 3)))], - [quote(context: TwoFieldStruct, do: nonempty_list(nonempty_improper_list(1, 4)))], - [quote(context: TwoFieldStruct, do: nonempty_list(nonempty_improper_list(2, 3)))], - [quote(context: TwoFieldStruct, do: nonempty_list(nonempty_improper_list(2, 4)))], - [quote(context: TwoFieldStruct, do: nonempty_list(nonempty_maybe_improper_list(5, 7)))], - [quote(context: TwoFieldStruct, do: nonempty_list(nonempty_maybe_improper_list(5, 8)))], - [quote(context: TwoFieldStruct, do: nonempty_list(nonempty_maybe_improper_list(6, 7)))], - [quote(context: TwoFieldStruct, do: nonempty_list(nonempty_maybe_improper_list(6, 8)))], - [quote(context: TwoFieldStruct, do: maybe_improper_list(nonempty_list(9), 11))], - [quote(context: TwoFieldStruct, do: maybe_improper_list(nonempty_list(9), 12))], - [quote(context: TwoFieldStruct, do: maybe_improper_list(nonempty_list(10), 11))], - [quote(context: TwoFieldStruct, do: maybe_improper_list(nonempty_list(10), 12))] + [ + quote( + context: TwoFieldStruct, + do: + nonempty_list(nonempty_improper_list(1 | atom(), 3 | 4) | nonempty_maybe_improper_list(5 | 6, 7 | atom())) + | maybe_improper_list(nonempty_list(9 | atom()), atom() | 12) + ) + ] ] ] assert %{TwoFieldStruct => map_idx_list_multitype(expected)} == read_types(types_file) end - test "resolve | within keyword list to list of lists", %{ + test "resolve a and b within keyword list [key: a | b]", %{ planner: planner, plan_file: plan_file, preconds_file: preconds_file, @@ -291,7 +249,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do [ quote( context: TwoFieldStruct, - do: [{:key1 | :key2, integer() | atom()}] + do: [key1: integer() | module()] ) ], planner @@ -301,17 +259,17 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do expected = [ [ - quote(context: TwoFieldStruct, do: [key1: integer()]), - quote(context: TwoFieldStruct, do: [key1: atom()]), - quote(context: TwoFieldStruct, do: [key2: integer()]), - quote(context: TwoFieldStruct, do: [key2: atom()]) + quote( + context: TwoFieldStruct, + do: [{:key1, integer() | atom()}] + ) ] ] assert %{TwoFieldStruct => map_idx_list_multitype(expected)} == read_types(types_file) end - test "resolve | within keyword(t) to list of keyword lists", %{ + test "resolve a and b within keyword(a | b)", %{ planner: planner, plan_file: plan_file, preconds_file: preconds_file, @@ -320,21 +278,20 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do ecto_assocs_file: ecto_assocs_file, t_reflections_file: t_reflections_file } do - plan_types([quote(context: TwoFieldStruct, do: keyword(integer() | float()))], planner) + plan_types([quote(context: TwoFieldStruct, do: keyword(module() | float()))], planner) :ok = Resolver.resolve(plan_file, preconds_file, types_file, deps_file, ecto_assocs_file, t_reflections_file, false) expected = [ [ - quote(context: TwoFieldStruct, do: [{atom(), integer()}]), - quote(context: TwoFieldStruct, do: [{atom(), float()}]) + quote(context: TwoFieldStruct, do: [{atom(), atom() | float()}]) ] ] assert %{TwoFieldStruct => map_idx_list_multitype(expected)} == read_types(types_file) end - test "resolve | within as_boolean(t) to list of t", %{ + test "resolve a and b within as_boolean(a | b)", %{ planner: planner, plan_file: plan_file, preconds_file: preconds_file, @@ -343,21 +300,20 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do ecto_assocs_file: ecto_assocs_file, t_reflections_file: t_reflections_file } do - plan_types([quote(context: TwoFieldStruct, do: as_boolean(integer() | float()))], planner) + plan_types([quote(context: TwoFieldStruct, do: as_boolean(integer() | module()))], planner) :ok = Resolver.resolve(plan_file, preconds_file, types_file, deps_file, ecto_assocs_file, t_reflections_file, false) expected = [ [ - quote(context: TwoFieldStruct, do: integer()), - quote(context: TwoFieldStruct, do: float()) + quote(context: TwoFieldStruct, do: integer() | atom()) ] ] assert %{TwoFieldStruct => map_idx_list_multitype(expected)} == read_types(types_file) end - test "resolve | within map to list of maps", %{ + test "resolve a and b within map %{ a | b => a | b}", %{ planner: planner, plan_file: plan_file, preconds_file: preconds_file, @@ -372,10 +328,11 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do context: TwoFieldStruct, do: %{ key1: %{ - required(atom() | integer()) => float() | neg_integer(), - optional(pid() | port()) => list() | tuple() + required(module() | integer()) => module() | neg_integer(), + optional(pid() | module()) => float() | module() }, - key2: 1 | 2 + key2: 1 | 2, + key3: %{(module() | integer()) => module() | neg_integer()} } ) ], @@ -385,82 +342,22 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do :ok = Resolver.resolve(plan_file, preconds_file, types_file, deps_file, ecto_assocs_file, t_reflections_file, false) expected = [ - [ - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => float(), optional(pid()) => [any()]}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => float(), optional(pid()) => [any()]}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => float(), optional(pid()) => tuple()}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => float(), optional(pid()) => tuple()}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => float(), optional(port()) => [any()]}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => float(), optional(port()) => [any()]}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => float(), optional(port()) => tuple()}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => float(), optional(port()) => tuple()}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => neg_integer(), optional(pid()) => [any()]}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => neg_integer(), optional(pid()) => [any()]}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => neg_integer(), optional(pid()) => tuple()}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => neg_integer(), optional(pid()) => tuple()}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => neg_integer(), optional(port()) => [any()]}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => neg_integer(), optional(port()) => [any()]}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => neg_integer(), optional(port()) => tuple()}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(atom()) => neg_integer(), optional(port()) => tuple()}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => float(), optional(pid()) => [any()]}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => float(), optional(pid()) => [any()]}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => float(), optional(pid()) => tuple()}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => float(), optional(pid()) => tuple()}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => float(), optional(port()) => [any()]}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => float(), optional(port()) => [any()]}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => float(), optional(port()) => tuple()}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => float(), optional(port()) => tuple()}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => neg_integer(), optional(pid()) => [any()]}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => neg_integer(), optional(pid()) => [any()]}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => neg_integer(), optional(pid()) => tuple()}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => neg_integer(), optional(pid()) => tuple()}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => neg_integer(), optional(port()) => [any()]}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => neg_integer(), optional(port()) => [any()]}, key2: 2}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => neg_integer(), optional(port()) => tuple()}, key2: 1}), - quote(context: TwoFieldStruct, do: %{key1: %{required(integer()) => neg_integer(), optional(port()) => tuple()}, key2: 2}) - ] - ] - - assert %{TwoFieldStruct => map_idx_list_multitype(expected)} == read_types(types_file) - end - - test "raise 4096 field type combinations max error giving 8*8*8*8*2 | types within map", %{ - planner: planner, - plan_file: plan_file, - preconds_file: preconds_file, - types_file: types_file, - deps_file: deps_file, - ecto_assocs_file: ecto_assocs_file, - t_reflections_file: t_reflections_file - } do - plan_types( [ quote( context: TwoFieldStruct, do: %{ - key1: atom() | integer() | float() | true | false | :one | :two | :three, - key2: atom() | integer() | float() | true | false | :one | :two | :three, - key3: atom() | integer() | float() | true | false | :one | :two | :three, - key4: atom() | integer() | float() | true | false | :one | :two | :three, - key5: atom() | integer() + key1: %{ + required(atom() | integer()) => atom() | neg_integer(), + optional(pid() | atom()) => float() | atom() + }, + key2: 1 | 2, + key3: %{(atom() | integer()) => atom() | neg_integer()} } ) - ], - planner - ) + ] + ] - module_file = ResolverTestHelper.env().file - - assert {:error, - [ - %Error{ - compiler_module: Resolver, - file: ^module_file, - struct_module: TwoFieldStruct, - message: - "Failed to generate 8192 type combinations with max. allowed 4096. Consider reducing number of | options or change the container type to struct using Domo." - } - ]} = Resolver.resolve(plan_file, preconds_file, types_file, deps_file, ecto_assocs_file, t_reflections_file, false) + assert %{TwoFieldStruct => map_idx_list_multitype(expected)} == read_types(types_file) end test "resolve | within ensurable struct to struct with any fields", %{ @@ -489,7 +386,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do assert %{TwoFieldStruct => map_idx_list_multitype(expected)} == read_types(types_file) end - test "resolve | within function arguments to list of functions", %{ + test "resolve a and b within function(a | b)", %{ planner: planner, plan_file: plan_file, preconds_file: preconds_file, @@ -499,7 +396,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do t_reflections_file: t_reflections_file } do plan_types( - [quote(context: TwoFieldStruct, do: (atom() | pid(), integer() | float() -> any()))], + [quote(context: TwoFieldStruct, do: (module() | pid(), integer() | float() -> any()))], planner ) @@ -507,10 +404,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do expected = [ [ - quote(context: TwoFieldStruct, do: (atom(), integer() -> any())), - quote(context: TwoFieldStruct, do: (atom(), float() -> any())), - quote(context: TwoFieldStruct, do: (pid(), integer() -> any())), - quote(context: TwoFieldStruct, do: (pid(), float() -> any())) + quote(context: TwoFieldStruct, do: (atom() | pid(), integer() | float() -> any())) ] ] @@ -532,8 +426,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do expected = [ [ - quote(context: TwoFieldStruct, do: false), - quote(context: TwoFieldStruct, do: true) + quote(context: TwoFieldStruct, do: true | false) ] ] @@ -555,9 +448,18 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do expected = [ [ - quote(context: TwoFieldStruct, do: pid()), - quote(context: TwoFieldStruct, do: port()), - quote(context: TwoFieldStruct, do: reference()) + { + :|, + [], + [ + {:pid, [], []}, + {:|, [], + [ + {:reference, [], []}, + {:port, [], []} + ]} + ] + } ] ] @@ -580,10 +482,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do expected = [ [ - quote(context: TwoFieldStruct, do: maybe_improper_list(0..255, <<_::_*8>>)), - quote(context: TwoFieldStruct, do: maybe_improper_list(0..255, [])), - quote(context: TwoFieldStruct, do: maybe_improper_list(<<_::_*8>>, <<_::_*8>>)), - quote(context: TwoFieldStruct, do: maybe_improper_list(<<_::_*8>>, [])) + quote(context: TwoFieldStruct, do: maybe_improper_list(0..255 | <<_::_*8>>, <<_::_*8>> | [])) ] ] @@ -605,11 +504,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do expected = [ [ - quote(context: TwoFieldStruct, do: <<_::_*8>>), - quote(context: TwoFieldStruct, do: maybe_improper_list(0..255, <<_::_*8>>)), - quote(context: TwoFieldStruct, do: maybe_improper_list(0..255, [])), - quote(context: TwoFieldStruct, do: maybe_improper_list(<<_::_*8>>, <<_::_*8>>)), - quote(context: TwoFieldStruct, do: maybe_improper_list(<<_::_*8>>, [])) + quote(context: TwoFieldStruct, do: <<_::_*8>> | maybe_improper_list(0..255 | <<_::_*8>>, <<_::_*8>> | [])) ] ] @@ -631,8 +526,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do expected = [ [ - quote(context: TwoFieldStruct, do: integer()), - quote(context: TwoFieldStruct, do: float()) + quote(context: TwoFieldStruct, do: integer() | float()) ] ] @@ -654,8 +548,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.OrTest do expected = [ [ - quote(context: TwoFieldStruct, do: :infinity), - quote(context: TwoFieldStruct, do: non_neg_integer()) + quote(context: TwoFieldStruct, do: :infinity | non_neg_integer()) ] ] diff --git a/test/domo/type_ensurer_factory/resolver/preconds_test.exs b/test/domo/type_ensurer_factory/resolver/preconds_test.exs index 1031220a..13bfedbc 100644 --- a/test/domo/type_ensurer_factory/resolver/preconds_test.exs +++ b/test/domo/type_ensurer_factory/resolver/preconds_test.exs @@ -1,4 +1,5 @@ -defmodule Domo.TypeEnsurerFactory.Resolver.PrecondsTest do[] +defmodule Domo.TypeEnsurerFactory.Resolver.PrecondsTest do + [] use Domo.FileCase, async: false use Placebo @@ -26,7 +27,10 @@ defmodule Domo.TypeEnsurerFactory.Resolver.PrecondsTest do[] ecto_assocs_file: ecto_assocs_file, t_reflections_file: t_reflections_file } do - File.write!(plan_file, TermSerializer.term_to_binary(%{filed_types_to_resolve: nil, environments: nil, remote_types_as_any_by_module: nil, t_reflections: nil})) + File.write!( + plan_file, + TermSerializer.term_to_binary(%{filed_types_to_resolve: nil, environments: nil, remote_types_as_any_by_module: nil, t_reflections: nil}) + ) assert {:error, [ @@ -125,8 +129,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.PrecondsTest do[] TwoFieldStruct => { %{ first: [ - {quote(do: %{key1: 1}), precond}, - {quote(do: %{key1: :none}), precond} + {quote(do: %{key1: {1 | :none, nil}}), precond} ] }, nil @@ -178,17 +181,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.PrecondsTest do[] quote( context: String, do: %{ - required({atom(), nil}) => {integer(), unquote(numbers_precond)}, - optional({<<_::_*8>>, unquote(strings_precond)}) => {unquote(atom_list_tuple), unquote(two_elem_tuple_precond)} - } - ), - nil - }, - { - quote( - context: String, - do: %{ - required({atom(), nil}) => {float(), unquote(numbers_precond)}, + required({atom(), nil}) => {{integer(), nil} | {float(), nil}, unquote(numbers_precond)}, optional({<<_::_*8>>, unquote(strings_precond)}) => {unquote(atom_list_tuple), unquote(two_elem_tuple_precond)} } ), @@ -295,12 +288,10 @@ defmodule Domo.TypeEnsurerFactory.Resolver.PrecondsTest do[] UserTypes => { %{ first: [ - {quote(do: integer()), precond1}, - {quote(do: float()), precond1} + {quote(do: {integer(), nil} | {float(), nil}), precond1} ], second: [ - {quote(do: %{key1: 1}), precond2}, - {quote(do: %{key1: :none}), precond2} + {quote(do: %{key1: {1 | :none, nil}}), precond2} ] }, nil @@ -400,12 +391,13 @@ defmodule Domo.TypeEnsurerFactory.Resolver.PrecondsTest do[] flush(planner) assert {:error, list} = Resolver.resolve(plan_file, preconds_file, types_file, deps_file, ecto_assocs_file, t_reflections_file, false) + assert [ - %Error{struct_module: UserTypes, message: "Precondition for value of identifier() type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of iodata() type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of iolist() type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of timeout() type is not allowed."} - ] = Enum.sort(list) + %Error{struct_module: UserTypes, message: "Precondition for value of identifier() type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of iodata() type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of iolist() type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of timeout() type is not allowed."} + ] = Enum.sort(list) end test "return precondition is not supported error for Ecto.Schema types", %{ @@ -438,14 +430,15 @@ defmodule Domo.TypeEnsurerFactory.Resolver.PrecondsTest do[] flush(planner) assert {:error, list} = Resolver.resolve(plan_file, preconds_file, types_file, deps_file, ecto_assocs_file, t_reflections_file, false) + assert [ - %Error{struct_module: UserTypes, message: "Precondition for value of Ecto.Schema.belongs_to(t) type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of Ecto.Schema.embeds_many(t) type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of Ecto.Schema.embeds_one(t) type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of Ecto.Schema.has_many(t) type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of Ecto.Schema.has_one(t) type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of Ecto.Schema.many_to_many(t) type is not allowed."}, - ] = Enum.sort(list) + %Error{struct_module: UserTypes, message: "Precondition for value of Ecto.Schema.belongs_to(t) type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of Ecto.Schema.embeds_many(t) type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of Ecto.Schema.embeds_one(t) type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of Ecto.Schema.has_many(t) type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of Ecto.Schema.has_one(t) type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of Ecto.Schema.many_to_many(t) type is not allowed."} + ] = Enum.sort(list) end test "return precondition is not supported error for primitive types", %{ @@ -492,20 +485,21 @@ defmodule Domo.TypeEnsurerFactory.Resolver.PrecondsTest do[] flush(planner) assert {:error, list} = Resolver.resolve(plan_file, preconds_file, types_file, deps_file, ecto_assocs_file, t_reflections_file, false) + assert [ - %Error{struct_module: UserTypes, message: "Precondition for value of %{} type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of 1 type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of :hello type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of <<>> type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of [] type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of boolean() type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of no_return() type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of none() type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of pid() type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of port() type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of reference() type is not allowed."}, - %Error{struct_module: UserTypes, message: "Precondition for value of {} type is not allowed."} - ] = Enum.sort(list) + %Error{struct_module: UserTypes, message: "Precondition for value of %{} type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of 1 type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of :hello type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of <<>> type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of [] type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of boolean() type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of no_return() type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of none() type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of pid() type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of port() type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of reference() type is not allowed."}, + %Error{struct_module: UserTypes, message: "Precondition for value of {} type is not allowed."} + ] = Enum.sort(list) end test "register precondition for any/term type", %{ diff --git a/test/domo/type_ensurer_factory/resolver/remote_test.exs b/test/domo/type_ensurer_factory/resolver/remote_test.exs index 926211c0..5680732c 100644 --- a/test/domo/type_ensurer_factory/resolver/remote_test.exs +++ b/test/domo/type_ensurer_factory/resolver/remote_test.exs @@ -128,7 +128,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.RemoteTest do assert %{RemoteUserType => expected} == read_types(types_file) end - test "resolve remote user type that has or | to list of primitive types", %{ + test "resolve a and b in remote user type that is a | b", %{ planner: planner, plan_file: plan_file, preconds_file: preconds_file, @@ -152,10 +152,7 @@ defmodule Domo.TypeEnsurerFactory.Resolver.RemoteTest do expected = types_content_empty_precond(%{ field: [ - quote(context: RemoteUserType, do: atom()), - quote(context: RemoteUserType, do: integer()), - quote(context: RemoteUserType, do: float()), - quote(context: RemoteUserType, do: [unquote({:any, [], []})]) + quote(context: RemoteUserType, do: atom() | integer() | float() | [unquote({:any, [], []})]) ] }) @@ -259,19 +256,21 @@ defmodule Domo.TypeEnsurerFactory.Resolver.RemoteTest do nonexisting_module_file = __ENV__.file assert {:error, list} = Resolver.resolve(plan_file, preconds_file, types_file, deps_file, ecto_assocs_file, t_reflections_file, false) + assert [ - %Error{ - compiler_module: Resolver, - file: nonexisting_module_file, - struct_module: NonexistingModule, - message: {:no_beam_file, NonexistingModule} - }, - %Error{ - compiler_module: Resolver, - file: remote_user_type_file, - struct_module: RemoteUserType, - message: {:type_not_found, {RemoteUserType, "nonexistent_type", "RemoteUserType.nonexistent_type()"}} - }] == Enum.sort(list) + %Error{ + compiler_module: Resolver, + file: nonexisting_module_file, + struct_module: NonexistingModule, + message: {:no_beam_file, NonexistingModule} + }, + %Error{ + compiler_module: Resolver, + file: remote_user_type_file, + struct_module: RemoteUserType, + message: {:type_not_found, {RemoteUserType, "nonexistent_type", "RemoteUserType.nonexistent_type()"}} + } + ] == Enum.sort(list) end end end diff --git a/test/domo_func_test.exs b/test/domo_func_test.exs index afdecfb1..db0ed6b6 100644 --- a/test/domo_func_test.exs +++ b/test/domo_func_test.exs @@ -32,6 +32,10 @@ defmodule DomoFuncTest do the following values should have types defined for fields of the Recipient struct: * Invalid value "mr" for field :title of %Recipient{}. Expected the value matching \ the :mr | :ms | :dr type. + Underlying errors: + - Expected the value matching the :mr type. + - Expected the value matching the :ms type. + - Expected the value matching the :dr type. * Invalid value 27.5 for field :age of %Recipient{}. Expected the value matching \ the integer() type.\ """, @@ -45,10 +49,7 @@ defmodule DomoFuncTest do """ the following values should have types defined for fields of the RecipientNestedOrTypes struct: * Invalid value %Recipient{__fields_pattern__} for field :title of %RecipientNestedOrTypes{}. \ - Expected the value matching the :mr | %Recipient{} | :dr type. - Underlying errors: - - Value of field :title is invalid due to Invalid value "mr" for field :title of %Recipient{}. \ - Expected the value matching the :mr | :ms | :dr type.\ + Expected the value matching the :mr | %Recipient{} | :dr type.\ """ |> Regex.escape() |> String.replace("__fields_pattern__", ".*age: 27.*") @@ -215,7 +216,11 @@ defmodule DomoFuncTest do assert error == [ title: """ Invalid value "mr" for field :title of %Recipient{}. Expected the value matching \ - the :mr | :ms | :dr type.\ + the :mr | :ms | :dr type. + Underlying errors: + - Expected the value matching the :mr type. + - Expected the value matching the :ms type. + - Expected the value matching the :dr type.\ """, age: """ Invalid value 27.5 for field :age of %Recipient{}. Expected the value matching \ @@ -259,7 +264,11 @@ defmodule DomoFuncTest do assert error == [ title: """ Invalid value nil for field :title of %Recipient{}. Expected the value matching \ - the :mr | :ms | :dr type.\ + the :mr | :ms | :dr type. + Underlying errors: + - Expected the value matching the :mr type. + - Expected the value matching the :ms type. + - Expected the value matching the :dr type.\ """ ] end diff --git a/test/domo_test.exs b/test/domo_test.exs index 7b025c86..3440f797 100644 --- a/test/domo_test.exs +++ b/test/domo_test.exs @@ -20,10 +20,12 @@ defmodule DomoTest do Airplane, Airplane.Seat, Article, + Apple, Arena, Book, Customer, EctoPassenger, + FruitBasket, Game, Leaf, LeafHolder, @@ -33,6 +35,7 @@ defmodule DomoTest do Library.Shelve, MemonlyStruct, Money, + Orange, Order, PostFieldAndNestedPrecond, PostFieldPrecond, @@ -179,7 +182,7 @@ defmodule DomoTest do end end - test "ensures data integrity of a struct with a sum type field" do + test "ensures data integrity of a struct with a sum | type field" do DomoMixTask.start_plan_collection() compile_game_struct() DomoMixTask.process_plan({:ok, []}, []) @@ -206,6 +209,28 @@ defmodule DomoTest do assert %{__struct__: Game} = %{game | status: {:wining_player, "player2"}} |> Game.ensure_type!() end + test "ensures data integrity of a struct with list field having sum | element type" do + DomoMixTask.start_plan_collection() + compile_fruit_structs() + DomoMixTask.process_plan({:ok, []}, []) + + apple = Apple.new!() + orange = Orange.new!() + + assert_raise ArgumentError, ~r/Invalid value nil for field :fruits/s, fn -> + _ = FruitBasket.new!(fruits: nil) + end + + basket = FruitBasket.new!(fruits: []) + + assert_raise ArgumentError, ~r/- The element at index 1 has value nil that is invalid./s, fn -> + _ = %{basket | fruits: [apple, nil]} |> FruitBasket.ensure_type!() + end + + FruitBasket.new!(fruits: [apple, orange]) + assert %{__struct__: FruitBasket} = %{basket | fruits: [apple, orange]} |> FruitBasket.ensure_type!() + end + test "ensures data integrity of a struct with a field referencing erlang type" do DomoMixTask.start_plan_collection() compile_web_service_struct() @@ -358,7 +383,7 @@ defmodule DomoTest do end end - test "ensures data integrity with struct field type's value preconditions" do + test "ensures data integrity with struct field type precondition" do DomoMixTask.start_plan_collection() compile_account_struct() {:ok, []} = DomoMixTask.process_plan({:ok, []}, []) @@ -461,9 +486,11 @@ a true value from the precondition.*defined for Account.t\(\) type./s, fn -> account = AccountOpaquePrecond.new!(id: 101) assert %{__struct__: AccountOpaquePrecond} = account - assert_raise ArgumentError, ~r/Expected the value matching the integer\(\) type. And a true value from the precondition function/s, fn -> - _ = AccountOpaquePrecond.new!(id: -500) - end + assert_raise ArgumentError, + ~r/Expected the value matching the integer\(\) | float\(\) type. And a true value from the precondition function/s, + fn -> + _ = AccountOpaquePrecond.new!(id: -500) + end assert_raise ArgumentError, ~r/Invalid value %AccountOpaquePrecond{id: 100}. Expected the value matching the AccountOpaquePrecond.t\(\) type. And a true value from the precondition function/s, @@ -1109,23 +1136,7 @@ a true value from the precondition.*defined for Account.t\(\) type./s, fn -> end describe "Domo library error messages should" do - test "have no underlying errors printed giving | type with primitive type arguments" do - DomoMixTask.start_plan_collection() - compile_receiver_struct() - DomoMixTask.process_plan({:ok, []}, []) - - assert_raise ArgumentError, - """ - the following values should have types defined for fields of the Receiver struct: - * Invalid value nil for field :title of %Receiver{}. Expected the value \ - matching the :mr | :ms | :dr type.\ - """, - fn -> - _ = Receiver.new!(title: nil, name: "ok") - end - end - - test "have only underlying error for matching argument type with failed precondition giving | with user type arguments" do + test "have underlying error printed for | sum type" do DomoMixTask.start_plan_collection() compile_money_struct() DomoMixTask.process_plan({:ok, []}, []) @@ -1136,8 +1147,9 @@ a true value from the precondition.*defined for Account.t\(\) type./s, fn -> * Invalid value 0.3 for field :amount of %Money{}. Expected the value \ matching the :none | float() | integer() type. Underlying errors: - - Expected the value matching the float() type. And a true value from the \ - precondition function "&(&1 > 0.5)" defined for Money.float_amount() type.\ + - Expected the value matching the :none type. + - Expected the value matching the float() type. And a true value from the precondition function \"&(&1 > 0.5)\" defined for Money.float_amount() type. + - Expected the value matching the integer() type.\ """, fn -> _ = Money.new!(amount: 0.3) @@ -1149,23 +1161,29 @@ a true value from the precondition.*defined for Account.t\(\) type./s, fn -> compile_article_struct() {:ok, _} = DomoMixTask.process_plan({:ok, []}, []) + field_type_str = + case ElixirVersion.version() do + [1, minor, _] when minor < 12 -> + ":none | {:simple, %{author: <<_::_*8>>, published: <<_::_*8>>}} | {:detail, <<_::_*8>> | %{author: <<_::_*8>>, published_updated: :never | <<_::_*8>>}}" + + [1, minor, _] when minor >= 12 -> + ":none\n| {:simple, %{author: <<_::_*8>>, published: <<_::_*8>>}}\n| {:detail, <<_::_*8>> | %{author: <<_::_*8>>, published_updated: :never | <<_::_*8>>}}" + end + assert_raise ArgumentError, """ the following values should have types defined for fields of the Article struct: * Invalid value {:detail, %{author: "John Smith", published_updated: {~D[2021-06-20], nil}}} \ - for field :metadata of %Article{}. Expected the value matching the \ - :none \ - | {:simple, %{author: <<_::_*8>>, published: <<_::_*8>>}} \ - | {:detail, <<_::_*8>>} \ - | {:detail, %{author: <<_::_*8>>, published_updated: :never}} \ - | {:detail, %{author: <<_::_*8>>, published_updated: <<_::_*8>>}} type. + for field :metadata of %Article{}. Expected the value matching the #{field_type_str} type. Underlying errors: + - Expected the value matching the :none type. + - The element at index 0 has value :detail that is invalid. + - Expected the value matching the :simple type. - The element at index 1 has value %{author: "John Smith", published_updated: {~D[2021-06-20], nil}} that is invalid. - - The field with key :published_updated has value {~D[2021-06-20], nil} that is invalid. - - Expected the value matching the :never type. - - The element at index 1 has value %{author: "John Smith", published_updated: {~D[2021-06-20], nil}} that is invalid. - - The field with key :published_updated has value {~D[2021-06-20], nil} that is invalid. - - Expected the value matching the <<_::_*8>> type.\ + - Expected the value matching the <<_::_*8>> type. + - The field with key :published_updated has value {~D[2021-06-20], nil} that is invalid. + - Expected the value matching the :never type. + - Expected the value matching the <<_::_*8>> type.\ """, fn -> _ = Article.new!(metadata: {:detail, %{author: "John Smith", published_updated: {~D[2021-06-20], nil}}}) @@ -1665,6 +1683,33 @@ a true value from the precondition.*defined for Account.t\(\) type./s, fn -> [path] end + defp compile_fruit_structs do + path = MixProject.out_of_project_tmp_path("/fruits.ex") + + File.write!(path, """ + defmodule Apple do + use Domo, skip_defaults: true + defstruct [] + @type t() :: %__MODULE__{} + end + + defmodule Orange do + use Domo, skip_defaults: true + defstruct [] + @type t() :: %__MODULE__{} + end + + defmodule FruitBasket do + use Domo, skip_defaults: true + defstruct fruits: [] + @type t() :: %__MODULE__{fruits: [Apple.t() | Orange.t()]} + end + """) + + CompilerHelpers.compile_with_elixir() + [path] + end + defp compile_game_with_string_status do path = MixProject.out_of_project_tmp_path("/game.ex") diff --git a/test/struct_modules/lib/user_types.ex b/test/struct_modules/lib/user_types.ex index 6ee06727..628831ac 100644 --- a/test/struct_modules/lib/user_types.ex +++ b/test/struct_modules/lib/user_types.ex @@ -7,7 +7,7 @@ defmodule ModuleNested do @moduledoc false @type mn_float :: float() - @type various_type :: atom() | integer() | float() | list() + @type various_type :: module() | integer() | float() | list() defmodule Module do @moduledoc false diff --git a/test/support/resolver_test_helper.ex b/test/support/resolver_test_helper.ex index ab990e40..24e749e1 100644 --- a/test/support/resolver_test_helper.ex +++ b/test/support/resolver_test_helper.ex @@ -291,7 +291,7 @@ defmodule ResolverTestHelper do updated_values -> updated_values end - {[{:->, meta, [updated_args, return_type]}], nil} + {[{:->, meta, [updated_args, add_empty_precond(return_type)]}], nil} end def add_empty_precond([{:->, _, _}] = value) do @@ -321,6 +321,16 @@ defmodule ResolverTestHelper do {{list_kind, [], updated_values}, nil} end + def add_empty_precond({:|, [], values}) do + updated_values = + case add_empty_precond(values) do + {updated_values, _} -> updated_values + updated_values -> updated_values + end + + {{:|, [], updated_values}, nil} + end + def add_empty_precond([]) do [] end @@ -387,10 +397,18 @@ defmodule ResolverTestHelper do {field_type, [], [add_empty_precond(value)]} end - def add_empty_precond_key(key) do + def add_empty_precond_key({:|, [], values}) do + {{:|, [], add_empty_precond(values)}, nil} + end + + def add_empty_precond_key(key) when is_atom(key) do key end + def add_empty_precond_key(key) do + {key, nil} + end + def read_types(types_file) do types_file |> File.read!()