From 64bee7690b953a079f8870fd639216dd12d1fdb1 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Wed, 9 Feb 2022 22:17:15 +0530 Subject: [PATCH 01/36] major upgrade --- .formatter.exs | 5 +- .gitignore | 4 - CHANGELOG.md | 4 + LICENSE.md | 2 +- bench/struct_bench.exs | 41 +- lib/construct.ex | 413 ++---------------- lib/construct/cast.ex | 50 +-- lib/construct/compiler.ex | 428 +++++++++++++++++++ lib/construct/compiler/ast.ex | 37 ++ lib/construct/compiler/ast/ast_types.ex | 175 ++++++++ lib/construct/hooks/map.ex | 15 + lib/construct/hooks/omit_default.ex | 31 ++ lib/construct/type.ex | 196 +-------- mix.exs | 17 +- mix.lock | 23 +- priv/.keep | 0 test/integration/compile_hook_test.exs | 129 ++++++ test/integration/hooks/map_test.exs | 32 ++ test/integration/hooks/omit_default_test.exs | 59 +++ test/support/test_case.ex | 28 +- 20 files changed, 1055 insertions(+), 634 deletions(-) create mode 100644 lib/construct/compiler.ex create mode 100644 lib/construct/compiler/ast.ex create mode 100644 lib/construct/compiler/ast/ast_types.ex create mode 100644 lib/construct/hooks/map.ex create mode 100644 lib/construct/hooks/omit_default.ex delete mode 100644 priv/.keep create mode 100644 test/integration/compile_hook_test.exs create mode 100644 test/integration/hooks/map_test.exs create mode 100644 test/integration/hooks/omit_default_test.exs diff --git a/.formatter.exs b/.formatter.exs index 71af955..30aa451 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,8 +1,7 @@ locals_without_parens = [ + structure_compile_hook: 1, structure_compile_hook: 2, structure: 1, - field: 1, - field: 2, - field: 3, + field: 1, field: 2, field: 3, include: 1 ] diff --git a/.gitignore b/.gitignore index e517dc8..6e1db0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,6 @@ # The directory Mix will write compiled artifacts to. /_build -# Bench results -/bench/graph -/bench/snapshots - # If you run "mix test --cover", coverage assets end up here. /cover diff --git a/CHANGELOG.md b/CHANGELOG.md index 905452d..31962b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Changelog +## v3.0.0 + + + ## v2.1.10 * Enhancements diff --git a/LICENSE.md b/LICENSE.md index 2199b14..5162a1a 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Unlimited Technologies +Copyright (c) 2022 Unlimited Technologies Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/bench/struct_bench.exs b/bench/struct_bench.exs index f958d75..4e56ac6 100644 --- a/bench/struct_bench.exs +++ b/bench/struct_bench.exs @@ -1,27 +1,26 @@ -defmodule StructBench do - use Benchfella +defmodule Embedded do + use Construct - defmodule Embedded do - use Construct - - structure do - field :e - end + structure do + field :e end +end - defmodule Example do - use Construct +defmodule Example do + use Construct - structure do - field :a - field :b, :float - field :c, {:map, :integer} - field :d, Embedded - end - end - - bench "complex" do - {:ok, %Example{}} = - Example.make(%{a: "test", b: 1.42, c: %{a: 0, b: 42}, d: %{e: "embeds"}}) + structure do + field :a + field :b, :float + field :c, {:map, :integer} + field :d, Embedded end end + +Benchee.run( + %{ + "make" => fn -> {:ok, _} = Example.make(%{a: "test", b: 1.42, c: %{a: 0, b: 42}, d: %{e: "embeds"}}) end, + }, + time: 10, + memory_time: 2 +) diff --git a/lib/construct.ex b/lib/construct.ex index 8a21f4a..efc1b5c 100644 --- a/lib/construct.ex +++ b/lib/construct.ex @@ -27,11 +27,23 @@ defmodule Construct do """ @type t :: struct - @type_checker_name Construct.TypeRegistry - @no_default :__construct_no_default__ - # elixir 1.9.0 do not raise deadlocks for Code.ensure_compiled/1 - @no_raise_on_deadlocks Version.compare(System.version(), "1.9.0") != :lt + @doc """ + Alias to `Construct.Cast.make/3`. + """ + @callback make(params :: map, opts :: Keyword.t) :: {:ok, t} | {:error, term} + + @doc """ + Alias to `c:make/2`, but raises an `Construct.MakeError` exception if params have errors. + """ + @callback make!(params :: map, opts :: Keyword.t) :: t + + @doc """ + Alias to `c:make/2`, used to follow `c:Construct.Type.cast/1` callback. + + To use this structure as custom type. + """ + @callback cast(params :: map, opts :: Keyword.t) :: {:ok, t} | {:error, term} @doc false defmacro __using__(opts \\ []) @@ -39,22 +51,13 @@ defmodule Construct do defmacro __using__({:%{}, _, _} = types) do quote do use Construct do - unquote(__ast_from_types__(types)) + unquote(Construct.Compiler.define_from_types(types)) end end end defmacro __using__(opts) when is_list(opts) do - {definition, opts} = Keyword.pop(opts, :do) - - pre_ast = - if definition do - defstructure(definition) - else - quote do - import Construct, only: [structure: 1] - end - end + {pre_ast, opts} = Construct.Compiler.pre(opts) quote do @behaviour Construct @@ -84,28 +87,8 @@ defmodule Construct do @doc """ Defines a structure. """ - defmacro structure([do: block]) do - defstructure(block) - end - - defp defstructure(block) do - quote do - import Construct - - Construct.__ensure_type_checker_started__() - Construct.__register_as_complex_type__(__MODULE__) - - Module.register_attribute(__MODULE__, :fields, accumulate: true) - Module.register_attribute(__MODULE__, :construct_fields, accumulate: true) - Module.register_attribute(__MODULE__, :construct_fields_enforce, accumulate: true) - - unquote(block) - - Module.eval_quoted __ENV__, {:__block__, [], [ - Construct.__defstruct__(@construct_fields, @construct_fields_enforce), - Construct.__types__(@fields), - Construct.__typespecs__(@fields)]} - end + defmacro structure([do: ast]) do + Construct.Compiler.define(ast) end @doc """ @@ -120,38 +103,8 @@ defmodule Construct do `Construct.DefinitionError` exception with detailed reason. """ @spec include(t, keyword) :: Macro.t() - defmacro include(struct, opts \\ []) do - quote do - module = unquote(struct) - - opts = unquote(opts) - only = Keyword.get(opts, :only) - - unless Construct.__is_construct_module__(module) do - raise Construct.DefinitionError, "provided #{inspect(module)} is not Construct module" - end - - types = module.__construct__(:types) - - types = - if is_list(only) do - Enum.each(only, fn(field) -> - unless Map.has_key?(types, field) do - raise Construct.DefinitionError, - "field #{inspect(field)} in :only option " <> - "doesn't exist in #{inspect(module)}" - end - end) - - Map.take(types, only) - else - types - end - - Enum.each(types, fn({name, {type, opts}}) -> - Construct.__field__(__MODULE__, name, type, opts) - end) - end + defmacro include(module, opts \\ []) do + Construct.Compiler.define_include(module, opts) end @doc """ @@ -179,328 +132,42 @@ defmodule Construct do By default this option is unset. Notice that you can't use functions as a default value. """ - @spec field(atom, Construct.Type.t, Keyword.t) :: Macro.t() + @spec field(atom(), Construct.Type.t(), Keyword.t()) :: Macro.t() defmacro field(name, type \\ :string, opts \\ []) - defmacro field(name, opts, [do: _] = contents) do - make_nested_field(name, contents, opts) + + defmacro field(name, opts, [do: _] = ast) do + Construct.Compiler.define_nested_field(name, ast, opts) end - defmacro field(name, [do: _] = contents, _opts) do - make_nested_field(name, contents, []) + + defmacro field(name, [do: _] = ast, _opts) do + Construct.Compiler.define_nested_field(name, ast, []) end + defmacro field(name, type, opts) do quote do - Construct.__field__(__MODULE__, unquote(name), unquote(type), unquote(opts)) + Construct.Compiler.define_field(__MODULE__, unquote(name), unquote(type), unquote(opts)) end end - @doc """ - Alias to `Construct.Cast.make/3`. - """ - @callback make(params :: map, opts :: Keyword.t) :: {:ok, t} | {:error, term} - - @doc """ - Alias to `c:make/2`, but raises an `Construct.MakeError` exception if params have errors. - """ - @callback make!(params :: map, opts :: Keyword.t) :: t - - @doc """ - Alias to `c:make/2`, used to follow `c:Construct.Type.cast/1` callback. - - To use this structure as custom type. - """ - @callback cast(params :: map, opts :: Keyword.t) :: {:ok, t} | {:error, term} + defmacro structure_compile_hook(type, [do: ast]) do + Construct.Compiler.define_structure_compile_hook(type, ast) + end @doc """ - Collects types from defined Construct module to map + Collect types from defined Construct module to map """ def types_of!(module) do - if construct_definition?(module) do - deep_collect_construct_types(module) + if construct?(module) do + Construct.Compiler.collect_types(module) else raise ArgumentError, "not a Construct definition" end end - defp deep_collect_construct_types(module) do - Enum.into(module.__construct__(:types), %{}, fn({name, {type, opts}}) -> - # check if type is not circular also - if module != type && is_atom(type) && construct_definition?(type) do - {name, {deep_collect_construct_types(type), opts}} - else - {name, {type, opts}} - end - end) - end - @doc """ - Checks if provided module is Construct definition + Checks if provided module is Construct module """ - def construct_definition?(module) do - ensure_compiled?(module) && function_exported?(module, :__construct__, 1) - end - - @doc false - def __ast_from_types__({:%{}, _, types}) do - Enum.reduce(Enum.reverse(types), [], fn - ({name, {{:%{}, _, _} = types, opts}}, acc) -> - [{:field, [], [name, opts, [do: {:__block__, [], __ast_from_types__(types)}]]} | acc] - - ({name, {:%{}, _, _} = types}, acc) -> - [{:field, [], [name, [], [do: {:__block__, [], __ast_from_types__(types)}]]} | acc] - - ({name, {type, opts}}, acc) -> - [{:field, [], [name, type, opts]} | acc] - - ({name, type}, acc) -> - [{:field, [], [name, type, []]} | acc] - - end) - end - - @doc false - def __defstruct__(construct_fields, construct_fields_enforce) do - {fields, enforce_fields} = - Enum.reduce(construct_fields, {[], construct_fields_enforce}, fn - ({key, value}, {fields, enforce}) when is_function(value) -> - {[{key, nil} | fields], [key | enforce]} - - (field, {fields, enforce}) -> - {[field | fields], enforce} - - end) - - fields = - fields - |> Enum.reverse() - |> Enum.uniq_by(fn({k, _}) -> k end) - |> Enum.reverse() - - quote do - enforce_keys = Keyword.get(@construct_opts, :enforce_keys, true) - - if enforce_keys do - @enforce_keys unquote(enforce_fields) - end - - defstruct unquote(Macro.escape(fields)) - end - end - - @doc false - def __types__(fields) do - fields = Enum.uniq_by(fields, fn({k, _v, _opts}) -> k end) - - types = - fields - |> Enum.into(%{}, fn({name, type, opts}) -> {name, {type, opts}} end) - |> Macro.escape - - quote do - def __construct__(:types), do: unquote(types) - end - end - - @doc false - def __typespecs__(fields) do - typespecs = - Enum.map(fields, fn({name, type, opts}) -> - type = Construct.Type.spec(type) - - type = - case Keyword.fetch(opts, :default) do - {:ok, default} -> - typeof_default = Construct.Type.typeof(default) - - if type == typeof_default do - type - else - quote do: unquote(type) | unquote(typeof_default) - end - - :error -> - type - end - - {name, type} - end) - - modulespec = - {:%, [], - [ - {:__MODULE__, [], Elixir}, - {:%{}, [], typespecs} - ]} - - quote do - @type t :: unquote(modulespec) - end - end - - @doc false - def __field__(mod, name, type, opts) do - check_field_name!(name) - check_type!(type) - - case default_for_struct(type, opts) do - @no_default -> - Module.put_attribute(mod, :fields, {name, type, opts}) - Module.put_attribute(mod, :construct_fields, {name, nil}) - Module.put_attribute(mod, :construct_fields_enforce, name) - - default -> - Module.put_attribute(mod, :fields, {name, type, Keyword.put(opts, :default, default)}) - Module.put_attribute(mod, :construct_fields, {name, default}) - pop_attribute(mod, :construct_fields_enforce, name) - - end - end - - @doc false - def __ensure_type_checker_started__ do - case Agent.start(fn -> MapSet.new end, name: @type_checker_name) do - {:ok, _pid} -> :ok - {:error, {:already_started, _pid}} -> :ok - _ -> raise Construct.DefinitionError, "unexpected compilation error" - end - end - - @doc false - def __register_as_complex_type__(module) do - Agent.update(@type_checker_name, &MapSet.put(&1, module)) - end - - @doc false - def __is_construct_module__(module) do - construct_module?(module) - end - - defp make_nested_field(name, contents, opts) do - check_field_name!(name) - - nested_module_name = String.to_atom(Macro.camelize(Atom.to_string(name))) - - quote do - opts = unquote(opts) - - current_module_name_ast = - __MODULE__ - |> Atom.to_string() - |> String.split(".") - |> Enum.map(&String.to_atom/1) - - derives = Keyword.get(opts, :derive, Module.get_attribute(__MODULE__, :derive)) - - current_module_ast = - {:__aliases__, [alias: false], current_module_name_ast ++ [unquote(nested_module_name)]} - |> Macro.expand(__ENV__) - - defmodule current_module_ast do - @derive derives - - use Construct do - unquote(contents) - end - end - - Construct.__field__(__MODULE__, unquote(name), current_module_ast, opts) - end - end - - defp pop_attribute(mod, key, value) do - old = Module.get_attribute(mod, key) - Module.delete_attribute(mod, key) - - Enum.each(old -- [value], &Module.put_attribute(mod, key, &1)) - end - - defp check_type!({:array, type}) do - check_type!(type) - end - defp check_type!({:map, type}) do - check_type!(type) - end - defp check_type!({typec, _arg}) do - check_typec_complex!(typec) - end - defp check_type!(type_list) when is_list(type_list) do - Enum.each(type_list, &check_type!/1) - end - defp check_type!(type) do - unless Construct.Type.primitive?(type), do: check_type_complex!(type) - end - - defp check_type_complex!(module) do - check_type_complex!(module, {:cast, 1}) - end - - defp check_typec_complex!(module) do - check_type_complex!(module, {:castc, 2}) - end - - defp check_type_complex!(module, {f, a}) do - unless construct_module?(module) do - unless ensure_compiled?(module) do - raise Construct.DefinitionError, "undefined module #{inspect(module)}" - end - - unless function_exported?(module, f, a) do - raise Construct.DefinitionError, "undefined function #{f}/#{a} for #{inspect(module)}" - end - end - end - - defp check_field_name!(name) when is_atom(name) do - :ok - end - defp check_field_name!(name) do - raise Construct.DefinitionError, "expected atom for field name, got `#{inspect(name)}`" - end - - defp default_for_struct(maybe_module, opts) when is_atom(maybe_module) do - case check_default!(Keyword.get(opts, :default, @no_default)) do - @no_default -> try_to_make_struct_instance(maybe_module) - val -> val - end - end - defp default_for_struct(_, opts) do - check_default!(Keyword.get(opts, :default, @no_default)) - end - - defp check_default!(default) when is_function(default, 0) do - default - end - defp check_default!(default) when is_function(default) do - raise Construct.DefinitionError, "functions in default values should be zero-arity" - end - defp check_default!(default) do - default - end - - defp try_to_make_struct_instance(module) do - if construct_module?(module) do - make_struct(module) - else - @no_default - end - end - - defp make_struct(module) do - struct!(module) - rescue - [ArgumentError, UndefinedFunctionError] -> @no_default - end - - defp construct_module?(module) do - if @no_raise_on_deadlocks, do: Code.ensure_compiled(module) - - Agent.get(@type_checker_name, &MapSet.member?(&1, module)) || - ensure_compiled?(module) && function_exported?(module, :__construct__, 1) - end - - defp ensure_compiled?(module) do - case Code.ensure_compiled(module) do - {:module, _} -> true - {:error, _} -> false - end + def construct?(module) do + Construct.Compiler.construct_module?(module) end end diff --git a/lib/construct/cast.ex b/lib/construct/cast.ex index 52b216f..6ce5203 100644 --- a/lib/construct/cast.ex +++ b/lib/construct/cast.ex @@ -67,9 +67,11 @@ defmodule Construct.Cast do """ @spec make(atom | types, map, options) :: {:ok, Construct.t | map} | {:error, term} def make(struct_or_types, params, opts \\ []) + def make(module, params, opts) when is_atom(module) do make_struct(make_struct_instance(module), collect_types(module), params, opts) end + def make(types, params, opts) do cast_params(types, params, opts) end @@ -96,36 +98,30 @@ defmodule Construct.Cast do @doc false defp make_struct(%{__struct__: _module} = struct, types, params, opts) do - make_map? = Keyword.get(opts, :make_map, false) - case cast_params(types, params, opts) do {:ok, changes} -> - if make_map? do - {:ok, changes} - else - {:ok, struct(struct, changes)} - end + {:ok, struct(struct, changes)} + {:error, errors} -> {:error, errors} end end defp cast_params(types, params, opts) do - empty_values = Keyword.get(opts, :empty_values, []) params = convert_params(params) types = convert_types(types) permitted = Map.keys(types) - case Enum.reduce(permitted, {%{}, %{}, true}, - &process_param(&1, params, types, empty_values, opts, &2)) do + case Enum.reduce(permitted, {%{}, %{}, true}, &process_param(&1, params, types, opts, &2)) do {changes, _errors, true} -> {:ok, changes} {_changes, errors, false} -> {:error, errors} end end - defp convert_params(%{__struct__: _} = params) do + defp convert_params(%_{} = params) do convert_params(Map.from_struct(params)) end + defp convert_params(params) when is_list(params) or is_map(params) do Enum.reduce(params, nil, fn ({key, _value}, nil) when is_binary(key) -> @@ -150,6 +146,7 @@ defmodule Construct.Cast do list -> Enum.into(list, %{}) end end + defp convert_params(params) do params end @@ -157,18 +154,20 @@ defmodule Construct.Cast do defp convert_types(types) when is_map(types) do types end + defp convert_types(types) when is_list(types) do Enum.into(types, %{}) end + defp convert_types(invalid_types) do raise Construct.Error, "expected types to be a {key, value} structure, got: #{inspect(invalid_types)}" end - defp process_param(key, params, types, empty_values, opts, {changes, errors, valid?}) do + defp process_param(key, params, types, opts, {changes, errors, valid?}) do param_key = Atom.to_string(key) {type, type_opts} = type!(key, types) - case cast_field(param_key, type, type_opts, params, empty_values, opts) do + case cast_field(param_key, type, type_opts, params, opts) do {:ok, value} -> {Map.put(changes, key, value), errors, valid?} {:error, reason} -> @@ -185,7 +184,7 @@ defmodule Construct.Cast do end end - defp cast_field(param_key, type, type_opts, params, empty_values, opts) do + defp cast_field(param_key, type, type_opts, params, opts) do default_value = Keyword.get(type_opts, :default, @default_value) error_values = Keyword.get(opts, :error_values, false) @@ -194,11 +193,7 @@ defmodule Construct.Cast do {:ok, value} %{^param_key => value} -> - if value in empty_values do - put_value(type, error_values, value, {:error, :missing}) - else - put_value(type, error_values, value, cast_field_value(type, value, empty_values, opts)) - end + put_value(type, error_values, value, cast_field_value(type, value, opts)) _ -> if default_value == @default_value do @@ -213,25 +208,26 @@ defmodule Construct.Cast do defp make_default_value(value) when is_function(value, 0) do value.() end + defp make_default_value(value) do value end - defp cast_field_value(type, value, empty_values, opts) do + defp cast_field_value(type, value, opts) do case Construct.Type.cast(type, value, opts) do {:ok, value} -> - if value in empty_values do - {:error, :missing} - else - {:ok, value} - end + {:ok, value} + {:error, reason} -> {:error, reason} + :error -> {:error, :invalid} + any -> - raise Construct.MakeError, "expected #{inspect(type)} to return {:ok, term} | {:error, term} | :error, " <> - "got an unexpected value: `#{inspect(any)}`" + raise Construct.MakeError, + "expected #{inspect(type)} to return {:ok, term} | {:error, term} | :error, " <> + "got an unexpected value: `#{inspect(any)}`" end end diff --git a/lib/construct/compiler.ex b/lib/construct/compiler.ex new file mode 100644 index 0000000..e927a5c --- /dev/null +++ b/lib/construct/compiler.ex @@ -0,0 +1,428 @@ +defmodule Construct.Compiler do + @moduledoc false + + alias Construct.Compiler + alias Construct.Compiler.AST + + @registry Construct.Registry + @no_default :__construct_no_default__ + + def construct_module?(module) do + registered_type?(module) || ensure_compiled?(module) && function_exported?(module, :__construct__, 1) + end + + def collect_types(module) do + Enum.into(module.__construct__(:types), %{}, fn({name, {type, opts}}) -> + # check if type is not circular also + if module != type && is_atom(type) && construct_module?(type) do + {name, {collect_types(type), opts}} + else + {name, {type, opts}} + end + end) + end + + def pre(opts) do + {pre_ast, opts} = + case Keyword.pop(opts, :do) do + {nil, opts} -> + {quote(do: import(Construct, only: [structure: 1, structure_compile_hook: 2])), opts} + + {ast, opts} -> + {Construct.Compiler.define(ast), opts} + end + + register_attributes_ast = + quote do + Module.register_attribute(__MODULE__, :fields, accumulate: true) + Module.register_attribute(__MODULE__, :construct_fields, accumulate: true) + Module.register_attribute(__MODULE__, :construct_fields_enforce, accumulate: true) + Module.register_attribute(__MODULE__, :construct_compile_hook_pre, accumulate: true) + Module.register_attribute(__MODULE__, :construct_compile_hook_post, accumulate: true) + end + + {AST.block([register_attributes_ast, pre_ast]), opts} + end + + def define(ast) do + quote do + Compiler.ensure_registry_started() + Compiler.register_type(__MODULE__) + + Module.put_attribute(__MODULE__, :construct_defined, true) + + Module.eval_quoted(__ENV__, AST.block( + Enum.reverse(Module.get_attribute(__MODULE__, :construct_compile_hook_pre)) + )) + + try do + import Construct, only: [ + field: 1, field: 2, field: 3, + include: 1, include: 2 + ] + + unquote(ast) + after + :ok + end + + Module.eval_quoted(__ENV__, AST.block([ + Compiler.define_struct(@construct_fields, @construct_fields_enforce), + Compiler.define_construct_functions(__ENV__, @fields), + Compiler.define_typespec(@fields), + ])) + + Module.eval_quoted(__ENV__, AST.block( + Enum.reverse(Module.get_attribute(__MODULE__, :construct_compile_hook_post)) + )) + end + end + + def define_from_types({:%{}, _, types}) do + Enum.reduce(Enum.reverse(types), [], fn + ({name, {{:%{}, _, _} = types, opts}}, acc) -> + [{:field, [], [name, opts, [do: AST.block(define_from_types(types))]]} | acc] + + ({name, {:%{}, _, _} = types}, acc) -> + [{:field, [], [name, [], [do: AST.block(define_from_types(types))]]} | acc] + + ({name, {type, opts}}, acc) -> + [{:field, [], [name, type, opts]} | acc] + + ({name, type}, acc) -> + [{:field, [], [name, type, []]} | acc] + + end) + end + + def define_struct(construct_fields, construct_fields_enforce) do + {fields, enforce_fields} = + Enum.reduce(construct_fields, {[], construct_fields_enforce}, fn + ({key, value}, {fields, enforce}) when is_function(value) -> + {[{key, nil} | fields], [key | enforce]} + + (field, {fields, enforce}) -> + {[field | fields], enforce} + + end) + + fields = + fields + |> Enum.reverse() + |> Enum.uniq_by(fn({k, _}) -> k end) + |> Enum.reverse() + + quote do + enforce_keys = Keyword.get(@construct_opts, :enforce_keys, true) + + if enforce_keys do + @enforce_keys unquote(enforce_fields) + end + + defstruct unquote(Macro.escape(fields)) + end + end + + def define_construct_functions(_env, fields) do + types = + fields + |> Enum.reduce(%{}, fn({name, type, opts}, acc) -> Map.put_new(acc, name, {type, opts}) end) + |> Macro.escape() + + quote do + def __construct__(:types), do: unquote(types) + end + end + + def define_typespec(fields) do + typespecs = + Enum.map(fields, fn({name, type, opts}) -> + {name, AST.spec_type(type, opts)} + end) + + modulespec = AST.spec_struct(typespecs) + + quote do + @type t :: unquote(modulespec) + end + end + + def define_field(module, name, type, opts) do + _ = check_field_name!(name) + k = check_type!(type) + + case default_value(k, type, opts) do + @no_default -> + put_attribute(module, :fields, {name, type, opts}) + put_attribute(module, :construct_fields, {name, nil}) + put_attribute(module, :construct_fields_enforce, name) + + term -> + put_attribute(module, :fields, {name, type, Keyword.put(opts, :default, term)}) + put_attribute(module, :construct_fields, {name, term}) + pop_attribute(module, :construct_fields_enforce, name) + + end + end + + def define_nested_field(name, ast, opts) do + check_field_name!(name) + + quote do + opts = unquote(opts) + module_name = AST.module_nest(__MODULE__, unquote(name)) + + derives_ast = Compiler.define_derive(__MODULE__, opts) + definition_pre_ast = Compiler.define_definition(__MODULE__, :pre, :construct_compile_hook_pre) + definition_post_ast = Compiler.define_definition(__MODULE__, :post, :construct_compile_hook_post) + + defmodule module_name do + use Construct + + Module.eval_quoted(__ENV__, derives_ast) + Module.eval_quoted(__ENV__, definition_pre_ast) + Module.eval_quoted(__ENV__, definition_post_ast) + + structure do + unquote(ast) + end + end + + Compiler.define_field(__MODULE__, unquote(name), module_name, opts) + end + end + + def define_definition(module, type, attribute) do + case Module.get_attribute(module, attribute) do + [] -> + AST.block([]) + + ls -> + quote do + structure_compile_hook unquote(type) do + unquote(ls) + end + end + end + end + + def define_derive(module, opts) do + derive = Keyword.get(opts, :derive, Module.get_attribute(module, :derive)) + construct_compile_hook_pre = Module.get_attribute(module, :construct_compile_hook_pre) + + if construct_compile_hook_pre == [] do + quote do + @derive unquote(derive) + end + end + end + + def define_include(module, opts) do + quote do + module = unquote(module) + + opts = unquote(opts) + only = Keyword.get(opts, :only) + + unless Compiler.construct_module?(module) do + raise Construct.DefinitionError, "provided #{inspect(module)} is not Construct module" + end + + types = module.__construct__(:types) + + types = + if is_list(only) do + Enum.each(only, fn(field) -> + unless Map.has_key?(types, field) do + raise Construct.DefinitionError, + "field #{inspect(field)} in :only option " <> + "doesn't exist in #{inspect(module)}" + end + end) + + Map.take(types, only) + else + types + end + + Enum.each(types, fn({name, {type, opts}}) -> + Compiler.define_field(__MODULE__, name, type, opts) + end) + end + end + + def define_structure_compile_hook(type, ast) do + unless type in [:pre, :post] do + raise Construct.DefinitionError, "structure_compile_hook type can be :pre or :past, but #{inspect(type)} given" + end + + ast_escaped = Macro.escape(ast) + + quote do + if Module.get_attribute(__MODULE__, :construct_defined) do + raise Construct.DefinitionError, "structure_compile_hook should be defined before structure itself" + end + + case unquote(type) do + :pre -> + Module.put_attribute(__MODULE__, :construct_compile_hook_pre, unquote(ast_escaped)) + + :post -> + Module.put_attribute(__MODULE__, :construct_compile_hook_post, unquote(ast_escaped)) + end + end + end + + def ensure_registry_started do + case Agent.start(&MapSet.new/0, name: @registry) do + {:ok, _pid} -> :ok + {:error, {:already_started, _pid}} -> :ok + _ -> raise Construct.DefinitionError, "unexpected compilation error" + end + end + + def register_type(module) do + Agent.update(@registry, &MapSet.put(&1, module)) + end + + def registered_type?(module) do + Agent.get(@registry, &MapSet.member?(&1, module)) + catch + :exit, _ -> false + end + + ### internal functions + + defp put_attribute(module, key, value) do + Module.put_attribute(module, key, value) + end + + defp pop_attribute(module, key, value) do + old = Module.get_attribute(module, key) + Module.delete_attribute(module, key) + + Enum.each(List.delete(old, value), &(put_attribute(module, key, &1))) + end + + defp check_type!({:array, type}) do + check_type!(type) + :builtin + end + + defp check_type!({:map, type}) do + check_type!(type) + :builtin + end + + defp check_type!({typec, _arg}) do + check_typec_complex!(typec) + :custom + end + + defp check_type!(type_list) when is_list(type_list) do + Enum.each(type_list, &check_type!/1) + :builtin + end + + defp check_type!(type) do + if Construct.Type.primitive?(type) do + :builtin + else + check_type_complex!(type) + end + end + + defp check_type_complex!(module) do + if construct_module?(module) do + :construct + else + unless ensure_compiled?(module) do + raise Construct.DefinitionError, "undefined module #{inspect(module)}" + end + + unless function_exported?(module, :cast, 1) do + raise Construct.DefinitionError, "undefined function cast/1 for #{inspect(module)}" + end + + :custom + end + end + + defp check_typec_complex!(module) do + unless ensure_compiled?(module) do + raise Construct.DefinitionError, "undefined module #{inspect(module)}" + end + + unless function_exported?(module, :castc, 2) do + raise Construct.DefinitionError, "undefined function castc/2 for #{inspect(module)}" + end + end + + defp check_field_name!(name) when is_atom(name) do + :ok + end + + defp check_field_name!(name) do + raise Construct.DefinitionError, "expected atom for field name, got `#{inspect(name)}`" + end + + defp default_value(kind, type, opts) do + check_default!(make_default_value(kind, type, opts)) + end + + defp make_default_value(:builtin, _type, opts) do + Keyword.get(opts, :default, @no_default) + end + + defp make_default_value(:construct, type, opts) do + case Keyword.get(opts, :default, @no_default) do + @no_default -> + if function_exported?(type, :default, 0) do + &type.default/0 + else + make_struct(type) + end + + term -> + term + end + end + + defp make_default_value(:custom, {type, _}, opts) do + make_default_value(:custom, type, opts) + end + + defp make_default_value(:custom, type, opts) do + case Keyword.get(opts, :default, @no_default) do + @no_default -> + if function_exported?(type, :default, 0) do + &type.default/0 + else + @no_default + end + + term -> + term + end + end + + defp check_default!(default) when is_function(default, 0) do + default + end + defp check_default!(default) when is_function(default) do + raise Construct.DefinitionError, "functions in default values should be zero-arity" + end + defp check_default!(default) do + default + end + + defp make_struct(module) do + module.make!() + rescue + [Construct.MakeError, UndefinedFunctionError] -> @no_default + end + + defp ensure_compiled?(module) do + Code.ensure_compiled(module) == {:module, module} + end +end diff --git a/lib/construct/compiler/ast.ex b/lib/construct/compiler/ast.ex new file mode 100644 index 0000000..86ffb7a --- /dev/null +++ b/lib/construct/compiler/ast.ex @@ -0,0 +1,37 @@ +defmodule Construct.Compiler.AST do + @moduledoc false + + def block(ast) do + {:__block__, [], List.wrap(ast)} + end + + def module_nest(module, name) do + Module.concat(module, String.to_atom(Macro.camelize(to_string(name)))) + end + + def spec_struct(ast) do + {:%, [], + [ + {:__MODULE__, [], Elixir}, + {:%{}, [], ast} + ]} + end + + def spec_type(type, opts) do + type = Construct.Compiler.AST.Types.spec(type) + + case Keyword.fetch(opts, :default) do + {:ok, default} -> + typeof_default = Construct.Compiler.AST.Types.typeof(default) + + if type == typeof_default do + type + else + quote do: unquote(type) | unquote(typeof_default) + end + + :error -> + type + end + end +end diff --git a/lib/construct/compiler/ast/ast_types.ex b/lib/construct/compiler/ast/ast_types.ex new file mode 100644 index 0000000..6658fca --- /dev/null +++ b/lib/construct/compiler/ast/ast_types.ex @@ -0,0 +1,175 @@ +defmodule Construct.Compiler.AST.Types do + @moduledoc false + + @builtin Construct.Type.builtin() + + @doc """ + Returns typespec AST for given type + + iex> spec([CommaList, {:array, :integer}]) |> Macro.to_string() + "list(:integer)" + + iex> spec({:array, :string}) |> Macro.to_string() + "list(String.t())" + + iex> spec({:map, CustomType}) |> Macro.to_string() + "%{optional(term) => CustomType.t()}" + + iex> spec(:string) |> Macro.to_string() + "String.t()" + + iex> spec(CustomType) |> Macro.to_string() + "CustomType.t()" + """ + @spec spec(Construct.Type.t()) :: Macro.t() + + def spec(type) when is_list(type) do + type |> List.last() |> spec() + end + + def spec({:array, type}) do + quote do + list(unquote(spec(type))) + end + end + + def spec({:map, type}) do + quote do + %{optional(term) => unquote(spec(type))} + end + end + + def spec({typec, _arg}) do + quote do + unquote(typec).t() + end + end + + def spec(:string) do + quote do + String.t() + end + end + + def spec(:decimal) do + quote do + Decimal.t() + end + end + + def spec(:utc_datetime) do + quote do + DateTime.t() + end + end + + def spec(:naive_datetime) do + quote do + NaiveDateTime.t() + end + end + + def spec(:date) do + quote do + Date.t() + end + end + + def spec(:time) do + quote do + Time.t() + end + end + + def spec(type) when type in @builtin do + type + end + + def spec(type) when is_atom(type) do + quote do + unquote(type).t() + end + end + + def spec(type) do + type + end + + @doc """ + Returns typespec AST for given term + + iex> typeof(nil) |> Macro.to_string() + "nil" + + iex> typeof(1.42) |> Macro.to_string() + "float()" + + iex> typeof("string") |> Macro.to_string() + "String.t()" + + iex> typeof(CustomType) |> Macro.to_string() + "CustomType.t()" + + iex> typeof(&NaiveDateTime.utc_now/0) |> Macro.to_string() + "NaiveDateTime.t()" + """ + @spec typeof(term()) :: Macro.t() + + def typeof(term) when is_nil(term) do + nil + end + + def typeof(term) when is_integer(term) do + {:integer, [], []} + end + + def typeof(term) when is_float(term) do + {:float, [], []} + end + + def typeof(term) when is_boolean(term) do + {:boolean, [], []} + end + + def typeof(term) when is_binary(term) do + quote do + String.t() + end + end + + def typeof(term) when is_pid(term) do + {:pid, [], []} + end + + def typeof(term) when is_reference(term) do + {:reference, [], []} + end + + def typeof(%{__struct__: struct}) when is_atom(struct) do + quote do + unquote(struct).t() + end + end + + def typeof(term) when is_map(term) do + {:map, [], []} + end + + def typeof(term) when is_atom(term) do + quote do + unquote(term).t() + end + end + + def typeof(term) when is_list(term) do + {:list, [], []} + end + + def typeof(term) when is_function(term, 0) do + typeof(term.()) + end + + def typeof(_) do + {:term, [], []} + end +end diff --git a/lib/construct/hooks/map.ex b/lib/construct/hooks/map.ex new file mode 100644 index 0000000..725f0df --- /dev/null +++ b/lib/construct/hooks/map.ex @@ -0,0 +1,15 @@ +defmodule Construct.Hooks.Map do + defmacro __using__(_opts \\ []) do + quote do + structure_compile_hook :post do + def make(params, opts) do + with {:ok, term} <- super(params, opts) do + {:ok, Map.from_struct(term)} + end + end + + defoverridable make: 2 + end + end + end +end diff --git a/lib/construct/hooks/omit_default.ex b/lib/construct/hooks/omit_default.ex new file mode 100644 index 0000000..673b780 --- /dev/null +++ b/lib/construct/hooks/omit_default.ex @@ -0,0 +1,31 @@ +defmodule Construct.Hooks.OmitDefault do + defmacro __using__(_opts \\ []) do + quote do + structure_compile_hook :post do + @omits Enum.reduce(@fields, [], fn({name, _type, opts}, acc) -> + if Keyword.get(opts, :omit_default) == true do + [{name, Keyword.get(opts, :default)} | acc] + else + acc + end + end) + + def make(params, opts) do + with {:ok, term} <- super(params, opts) do + term = + Enum.reduce(@omits, Map.from_struct(term), fn({name, default}, term) -> + case term do + %{^name => ^default} -> Map.delete(term, name) + %{} -> term + end + end) + + {:ok, term} + end + end + + defoverridable make: 2 + end + end + end +end diff --git a/lib/construct/type.ex b/lib/construct/type.ex index 998b49c..9b2a340 100644 --- a/lib/construct/type.ex +++ b/lib/construct/type.ex @@ -38,6 +38,14 @@ defmodule Construct.Type do ## Functions + def builtin do + @builtin + end + + def builtinc do + @builtinc + end + @doc """ Checks if we have a primitive type. @@ -104,14 +112,14 @@ defmodule Construct.Type do iex> cast(:binary, "beef") {:ok, "beef"} - iex> cast(:decimal, Decimal.new(1.0)) - {:ok, Decimal.new(1.0)} + iex> cast(:decimal, Decimal.from_float(1.0)) + {:ok, Decimal.from_float(1.0)} iex> cast(:decimal, Decimal.new("1.0")) - {:ok, Decimal.new(1.0)} + {:ok, Decimal.from_float(1.0)} iex> cast(:decimal, 1.0) - {:ok, Decimal.new(1.0)} + {:ok, Decimal.from_float(1.0)} iex> cast(:decimal, "1.0") - {:ok, Decimal.new(1.0)} + {:ok, Decimal.from_float(1.0)} iex> cast({:array, :integer}, [1, 2, 3]) {:ok, [1, 2, 3]} @@ -146,6 +154,7 @@ defmodule Construct.Type do else type.cast(term) end + true -> cast(type, term) end @@ -205,8 +214,7 @@ defmodule Construct.Type do def cast(:reference, term) when is_reference(term), do: {:ok, term} def cast(:decimal, term) when is_binary(term) do - apply(Decimal, :parse, [term]) - |> validate_decimal() + validate_decimal(apply(Decimal, :parse, [term])) end def cast(:decimal, term) when is_integer(term) do {:ok, apply(Decimal, :new, [term])} @@ -245,8 +253,10 @@ defmodule Construct.Type do cond do not primitive?(type) -> type.cast(term) + of_base_type?(type, term) -> {:ok, term} + true -> :error end @@ -384,178 +394,6 @@ defmodule Construct.Type do end end - ## Typespecs - - @doc """ - Returns typespec AST for given type - - iex> spec([CommaList, {:array, :integer}]) |> Macro.to_string() - "list(:integer)" - - iex> spec({:array, :string}) |> Macro.to_string() - "list(String.t())" - - iex> spec({:map, CustomType}) |> Macro.to_string() - "%{optional(term) => CustomType.t()}" - - iex> spec(:string) |> Macro.to_string() - "String.t()" - - iex> spec(CustomType) |> Macro.to_string() - "CustomType.t()" - """ - @spec spec(t) :: Macro.t() - - def spec(type) when is_list(type) do - type |> List.last() |> spec() - end - - def spec({:array, type}) do - quote do - list(unquote(spec(type))) - end - end - - def spec({:map, type}) do - quote do - %{optional(term) => unquote(spec(type))} - end - end - - def spec({typec, _arg}) do - quote do - unquote(typec).t() - end - end - - def spec(:string) do - quote do - String.t() - end - end - - def spec(:decimal) do - quote do - Decimal.t() - end - end - - def spec(:utc_datetime) do - quote do - DateTime.t() - end - end - - def spec(:naive_datetime) do - quote do - NaiveDateTime.t() - end - end - - def spec(:date) do - quote do - Date.t() - end - end - - def spec(:time) do - quote do - Time.t() - end - end - - def spec(type) when type in @builtin do - type - end - - def spec(type) when is_atom(type) do - quote do - unquote(type).t() - end - end - - def spec(type) do - type - end - - @doc """ - Returns typespec AST for given term - - iex> typeof(nil) |> Macro.to_string() - "nil" - - iex> typeof(1.42) |> Macro.to_string() - "float()" - - iex> typeof("string") |> Macro.to_string() - "String.t()" - - iex> typeof(CustomType) |> Macro.to_string() - "CustomType.t()" - - iex> typeof(&NaiveDateTime.utc_now/0) |> Macro.to_string() - "NaiveDateTime.t()" - """ - @spec typeof(t) :: Macro.t() - - def typeof(term) when is_nil(term) do - nil - end - - def typeof(term) when is_integer(term) do - {:integer, [], []} - end - - def typeof(term) when is_float(term) do - {:float, [], []} - end - - def typeof(term) when is_boolean(term) do - {:boolean, [], []} - end - - def typeof(term) when is_binary(term) do - quote do - String.t() - end - end - - def typeof(term) when is_pid(term) do - {:pid, [], []} - end - - def typeof(term) when is_reference(term) do - {:reference, [], []} - end - - def typeof(%{__struct__: struct}) when is_atom(struct) do - quote do - unquote(struct).t() - end - end - - def typeof(term) when is_map(term) do - {:map, [], []} - end - - def typeof(term) when is_atom(term) do - quote do - unquote(term).t() - end - end - - def typeof(term) when is_list(term) do - {:list, [], []} - end - - def typeof(term) when is_function(term, 0) do - term.() |> typeof() - end - - def typeof(_) do - {:term, [], []} - end - ## Helpers defp validate_decimal({:ok, %{__struct__: Decimal, coef: coef}}) when coef in [:inf, :qNaN, :sNaN], diff --git a/mix.exs b/mix.exs index 35bfd95..7a8cafa 100644 --- a/mix.exs +++ b/mix.exs @@ -4,10 +4,11 @@ defmodule Construct.Mixfile do def project do [ app: :construct, - version: "2.1.10", - elixir: "~> 1.5", + version: "3.0.0", + elixir: "~> 1.9", deps: deps(), elixirc_paths: elixirc_paths(Mix.env), + consolidate_protocols: Mix.env() != :test, dialyzer: [ plt_file: {:no_warn, "_build/dialyzer.plt"} ], @@ -33,12 +34,12 @@ defmodule Construct.Mixfile do defp deps do [ - {:decimal, "~> 1.5", only: [:dev, :test]}, - {:benchfella, "~> 0.3", only: [:dev, :test]}, - {:dialyxir, "~> 1.0.0-rc.7", only: [:dev, :test], runtime: false}, - {:earmark, "~> 1.2", only: :dev}, - {:ex_doc, "~> 0.19", only: :dev}, - {:jason, "~> 1.1", only: :test} + {:decimal, "~> 1.6 or ~> 2.0", only: [:dev, :test]}, + {:benchee, "~> 1.0", only: [:dev, :test]}, + {:dialyxir, "~> 1.1.0", only: [:dev, :test], runtime: false}, + {:earmark, "~> 1.4", only: :dev}, + {:ex_doc, "~> 0.28", only: :dev}, + {:jason, "~> 1.3", only: :test} ] end diff --git a/mix.lock b/mix.lock index 1a8b110..0fe9fba 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,15 @@ %{ - "benchfella": {:hex, :benchfella, "0.3.5", "b2122c234117b3f91ed7b43b6e915e19e1ab216971154acd0a80ce0e9b8c05f5", [:mix], [], "hexpm"}, - "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"}, - "dialyxir": {:hex, :dialyxir, "1.0.0-rc.7", "6287f8f2cb45df8584317a4be1075b8c9b8a69de8eeb82b4d9e6c761cf2664cd", [:mix], [{:erlex, ">= 0.2.5", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, - "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, - "erlex": {:hex, :erlex, "0.2.5", "e51132f2f472e13d606d808f0574508eeea2030d487fc002b46ad97e738b0510", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, - "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, + "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"}, + "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, + "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, + "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, + "earmark": {:hex, :earmark, "1.4.19", "3854a17305c880cc46305af15fb1630568d23a709aba21aaa996ced082fc29d7", [:mix], [{:earmark_parser, ">= 1.4.18", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "d5a8c9f9e37159a8fdd3ea8437fb4e229eaf56d5129b9a011dc4780a4872079d"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.28.0", "7eaf526dd8c80ae8c04d52ac8801594426ae322b52a6156cd038f30bafa8226f", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e55cdadf69a5d1f4cfd8477122ebac5e1fadd433a8c1022dafc5025e48db0131"}, + "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.1", "264fc6864936b59fedb3ceb89998c64e9bb91945faf1eb115d349b96913cc2ef", [:mix], [], "hexpm", "23c31d0ec38c97bf9adde35bc91bc8e1181ea5202881f48a192f4aa2d2cf4d59"}, } diff --git a/priv/.keep b/priv/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/test/integration/compile_hook_test.exs b/test/integration/compile_hook_test.exs new file mode 100644 index 0000000..c0e4e7d --- /dev/null +++ b/test/integration/compile_hook_test.exs @@ -0,0 +1,129 @@ +defmodule Construct.Integration.CompileHookTest do + use Construct.TestCase + + test "compile hook pass derive down to nested structs" do + module = create_module do + use Construct + + structure_compile_hook :pre do + @derive {Jason.Encoder, []} + end + + structure do + field :a + + field :b do + field :ba do + field :baa, :string, default: "test" + end + + field :bb, :integer, default: 42 + end + + field :d do + field :da do + field :daa, :integer, default: 0 + end + end + end + end + + assert {:ok, structure} = make(module, a: "string") + + assert ~s({"a":"string","b":{"ba":{"baa":"test"},"bb":42},"d":{"da":{"daa":0}}}) + == Jason.encode!(structure) + end + + test "compile hook pass its ast down to nested structs" do + module = {_, name, _, _} = create_module do + use Construct + + structure_compile_hook :post do + def make(params, opts) do + with {:ok, struct} <- super(params, opts) do + {:ok, Map.from_struct(struct)} + end + end + end + + structure do + field :a + + field :b do + field :ba do + field :baa, :string, default: "test" + end + + field :bb, :integer, default: 42 + end + + field :d do + field :da do + field :daa, :integer, default: 0 + end + end + end + end + + assert {:ok, %{ + a: "string", + b: %{ba: %{baa: "test"}, bb: 42}, + d: %{da: %{daa: 0}} + }} == make(module, a: "string") + + assert %{ + __struct__: name, + a: "string", + b: %{ba: %{baa: "test"}, bb: 42}, + d: %{da: %{daa: 0}} + } == struct!(name, a: "string") + end + + test "compile hook can be called twice" do + module = create_module do + use Construct + + structure_compile_hook :post do + def make(params, opts) do + with {:ok, struct} <- super(params, opts) do + {:ok, %{struct | number: struct.number + 4}} + end + end + + defoverridable [make: 2] + end + + structure_compile_hook :post do + def make(params, opts) do + with {:ok, struct} <- super(params, opts) do + {:ok, %{struct | number: struct.number * 10}} + end + end + + defoverridable [make: 2] + end + + structure do + field :number, :integer + end + end + + assert {:ok, %{number: 150}} = make(module, number: 11) + end + + test "throws error when trying to use structure_compile_hook/2 after structure/1" do + assert_raise(Construct.DefinitionError, ~s(structure_compile_hook should be defined before structure itself), fn -> + create_module do + use Construct + + structure do + field :test + end + + structure_compile_hook :post do + # ... + end + end + end) + end +end diff --git a/test/integration/hooks/map_test.exs b/test/integration/hooks/map_test.exs new file mode 100644 index 0000000..fc4b63c --- /dev/null +++ b/test/integration/hooks/map_test.exs @@ -0,0 +1,32 @@ +defmodule Construct.Integration.Hooks.MapTest do + use Construct.TestCase + + test "makes map from structs" do + module = create_module do + use Construct + use Construct.Hooks.Map + + structure do + field :a + + field :b do + field :ba, :string, default: "!" + + field :bb do + field :bba, :string, default: "?" + end + end + end + end + + assert {:ok, %{ + a: "test", + b: %{ba: "!", bb: %{bba: "?"}} + }} == make(module, %{a: "test"}) + + assert {:ok, %{ + a: "test", + b: %{ba: "!", bb: %{bba: "#"}} + }} == make(module, %{a: "test", b: %{bb: %{bba: "#"}}}) + end +end diff --git a/test/integration/hooks/omit_default_test.exs b/test/integration/hooks/omit_default_test.exs new file mode 100644 index 0000000..9b10095 --- /dev/null +++ b/test/integration/hooks/omit_default_test.exs @@ -0,0 +1,59 @@ +defmodule Construct.Integration.Hooks.OmitDefaultTest do + use Construct.TestCase + + test "omit default values" do + module = create_module do + use Construct + use Construct.Hooks.OmitDefault + + structure do + field :a + + field :b, [omit_default: true] do + field :ba, :string, default: "!" + end + + field :d do + field :da, :string, default: nil, omit_default: true + end + + field :c, [omit_default: true] do + field :ca, :string, default: nil + field :cb, :string, default: nil, omit_default: true + end + end + end + + assert {:ok, %{ + a: "test", + d: %{} + }} == make(module, %{a: "test"}) + + assert {:ok, %{ + a: "test", + d: %{} + }} == make(module, %{a: "test", b: %{}, d: %{}}) + + assert {:ok, %{ + a: "test", + d: %{da: "test"} + }} == make(module, %{a: "test", b: %{}, d: %{da: "test"}}) + + assert {:ok, %{ + a: "test", + d: %{} + }} == make(module, %{a: "test", c: %{}}) + + assert {:ok, %{ + a: "test", + d: %{}, + c: %{ca: "test"} + }} == make(module, %{a: "test", c: %{ca: "test"}}) + + assert {:ok, %{ + a: "test", + d: %{}, + c: %{ca: nil, cb: "test"} + }} == make(module, %{a: "test", c: %{cb: "test"}}) + end +end diff --git a/test/support/test_case.ex b/test/support/test_case.ex index f63ba59..d095ac6 100644 --- a/test/support/test_case.ex +++ b/test/support/test_case.ex @@ -19,15 +19,31 @@ defmodule Construct.TestCase do end end + defmacro create_module([do: block]) do + create_test_module(block) + end + defmacro create_construct([do: block]) do - __create_construct__([], block) + create_test_module(quote do + use Construct + + structure do + unquote(block) + end + end) end defmacro create_construct(opts, [do: block]) do - __create_construct__(opts, block) + create_test_module(quote do + use Construct, unquote(opts) + + structure do + unquote(block) + end + end) end - def __create_construct__(opts, block) do + defp create_test_module(ast) do quote do # retrieve module name from test case and line name = __ENV__.module @@ -36,11 +52,7 @@ defmodule Construct.TestCase do module_name = :"#{name}_#{line}" defmodule module_name do - use Construct, unquote(opts) - - structure do - unquote(block) - end + unquote(ast) end end end From 103ec071accc63ba7b5a46ae74485d94523fc00d Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Fri, 20 May 2022 13:02:05 +0300 Subject: [PATCH 02/36] prepare for the first rc --- CHANGELOG.md | 9 ++++++++- lib/construct.ex | 3 +++ mix.exs | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31962b1..f470d0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,14 @@ ## v3.0.0 - +* Enhancements + * Add `structure_compile_hook/2` macro to inject code into structures + See `Construct.Hooks.Map` and `Construct.Hooks.OmitDefault` for examples + * Compiler optimization and refactor + +* Hard-deprecations + * Remove `make_map` option + * Remove `empty_values` option ## v2.1.10 diff --git a/lib/construct.ex b/lib/construct.ex index efc1b5c..e969ad4 100644 --- a/lib/construct.ex +++ b/lib/construct.ex @@ -149,6 +149,9 @@ defmodule Construct do end end + @doc """ + No doc at this time, should be written for 3.0.0 release + """ defmacro structure_compile_hook(type, [do: ast]) do Construct.Compiler.define_structure_compile_hook(type, ast) end diff --git a/mix.exs b/mix.exs index 7a8cafa..91cf28d 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Construct.Mixfile do def project do [ app: :construct, - version: "3.0.0", + version: "3.0.0-rc.0", elixir: "~> 1.9", deps: deps(), elixirc_paths: elixirc_paths(Mix.env), From a5fef753926fb093f3810add1aaa36dbfde1bc6e Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Thu, 26 May 2022 13:21:29 +0300 Subject: [PATCH 03/36] add github ci --- .github/workflows/ci.yml | 81 ++++++++++++++++++++++++++++++++++++++++ mix.exs | 6 ++- mix.lock | 9 +++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a190964 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,81 @@ +name: CI + +on: + pull_request: + push: + branches: + - master + +jobs: + test: + name: Test (Elixir ${{matrix.elixir}} | Erlang/OTP ${{matrix.otp}}) + runs-on: ubuntu-18.04 + strategy: + fail-fast: false + matrix: + include: + - otp: 24.3 + elixir: 1.13 + coverage: true + lint: true + - otp: 23.3 + elixir: 1.12 + - otp: 22.3 + elixir: 1.11 + - otp: 21.3 + elixir: 1.10 + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + MIX_ENV: test + + steps: + - name: Clone the repository + uses: actions/checkout@v2 + + - name: Start Docker + run: docker-compose up --detach + + - name: Install OTP and Elixir + uses: erlef/setup-beam@v1 + with: + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} + + - name: Cache dependencies + id: cache-deps + uses: actions/cache@v2 + with: + path: | + deps + _build + key: ${{ runner.os }}-mix-otp${{ matrix.otp }}-elixir${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} + + - name: Install and compile dependencies + if: steps.cache-deps.outputs.cache-hit != 'true' + run: | + mix deps.get --only test + mix deps.compile + + # - name: Check formatting + # run: mix format --check-formatted + # if: ${{ matrix.lint }} + + - name: Check no unused dependencies + run: mix deps.get && mix deps.unlock --check-unused + if: ${{ matrix.lint == 'true' && steps.cache-deps.outputs.cache-hit != 'true' }} + + - name: Compile with --warnings-as-errors + run: mix compile --warnings-as-errors + if: ${{ matrix.lint }} + + - name: Run tests + run: mix test --trace + if: ${{ !matrix.coverage }} + + - name: Run tests with coverage + run: mix coveralls.github + if: ${{ matrix.coverage }} + + - name: Dump Docker logs on failure + uses: jwalton/gh-docker-logs@v1 + if: failure() diff --git a/mix.exs b/mix.exs index 91cf28d..d296afd 100644 --- a/mix.exs +++ b/mix.exs @@ -13,6 +13,9 @@ defmodule Construct.Mixfile do plt_file: {:no_warn, "_build/dialyzer.plt"} ], + # Tests + test_coverage: [tool: ExCoveralls], + # Hex description: description(), package: package(), @@ -39,7 +42,8 @@ defmodule Construct.Mixfile do {:dialyxir, "~> 1.1.0", only: [:dev, :test], runtime: false}, {:earmark, "~> 1.4", only: :dev}, {:ex_doc, "~> 0.28", only: :dev}, - {:jason, "~> 1.3", only: :test} + {:jason, "~> 1.3", only: :test}, + {:excoveralls, "~> 0.14", only: :test} ] end diff --git a/mix.lock b/mix.lock index 0fe9fba..c36206e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,6 @@ %{ "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"}, + "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, @@ -7,9 +8,17 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.28.0", "7eaf526dd8c80ae8c04d52ac8801594426ae322b52a6156cd038f30bafa8226f", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e55cdadf69a5d1f4cfd8477122ebac5e1fadd433a8c1022dafc5025e48db0131"}, + "excoveralls": {:hex, :excoveralls, "0.14.5", "5c685449596e962c779adc8f4fb0b4de3a5b291c6121097572a3aa5400c386d3", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9b4a9bf10e9a6e48b94159e13b4b8a1b05400f17ac16cc363ed8734f26e1f4e"}, + "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.1", "264fc6864936b59fedb3ceb89998c64e9bb91945faf1eb115d349b96913cc2ef", [:mix], [], "hexpm", "23c31d0ec38c97bf9adde35bc91bc8e1181ea5202881f48a192f4aa2d2cf4d59"}, + "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } From 5b435f395f0d651c97747303c241d5eab9a78960 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Thu, 26 May 2022 13:24:28 +0300 Subject: [PATCH 04/36] remove travis-ci --- .travis.yml | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 322717e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: elixir - -dist: trusty -sudo: false - -matrix: - include: - - elixir: 1.9.1 - otp_release: 22.0.7 - - elixir: 1.8.2 - otp_release: 21.3.8 - - elixir: 1.7.2 - otp_release: 21.0 - - elixir: 1.7.2 - otp_release: 20.3.1 - - elixir: 1.6.6 - otp_release: 19.3 - - elixir: 1.5.3 - otp_release: 18.3 - - elixir: 1.5.3 - otp_release: 17.5 From 8eff3f954f9a582a503a9770acfac1c4f9d9f39a Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Thu, 26 May 2022 13:29:13 +0300 Subject: [PATCH 05/36] fix ci --- .github/workflows/ci.yml | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a190964..f678538 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,37 +32,20 @@ jobs: - name: Clone the repository uses: actions/checkout@v2 - - name: Start Docker - run: docker-compose up --detach - - name: Install OTP and Elixir uses: erlef/setup-beam@v1 with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} - - name: Cache dependencies - id: cache-deps - uses: actions/cache@v2 - with: - path: | - deps - _build - key: ${{ runner.os }}-mix-otp${{ matrix.otp }}-elixir${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} - - name: Install and compile dependencies - if: steps.cache-deps.outputs.cache-hit != 'true' run: | mix deps.get --only test mix deps.compile - # - name: Check formatting - # run: mix format --check-formatted - # if: ${{ matrix.lint }} - - name: Check no unused dependencies run: mix deps.get && mix deps.unlock --check-unused - if: ${{ matrix.lint == 'true' && steps.cache-deps.outputs.cache-hit != 'true' }} + if: ${{ matrix.lint == 'true' }} - name: Compile with --warnings-as-errors run: mix compile --warnings-as-errors From 0c9dff97c2fe88e81805fefb35355e021bf6a22f Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Thu, 26 May 2022 13:47:58 +0300 Subject: [PATCH 06/36] remove tests with deprecated behaviour --- lib/construct/cast.ex | 14 +------------- lib/construct/type.ex | 2 +- test/construct/cast_test.exs | 4 ---- test/integration/build_test.exs | 27 ++++++-------------------- test/integration/make_test.exs | 34 +-------------------------------- 5 files changed, 9 insertions(+), 72 deletions(-) diff --git a/lib/construct/cast.ex b/lib/construct/cast.ex index 6ce5203..6a01bc7 100644 --- a/lib/construct/cast.ex +++ b/lib/construct/cast.ex @@ -9,7 +9,7 @@ defmodule Construct.Cast do @type type :: {Construct.Type.t, Keyword.t} @type types :: %{required(atom) => type} - @type options :: [error_values: boolean, make_map: boolean, empty_values: list(term)] + @type options :: [error_values: boolean] @doc """ Function to compose structure instance from params: @@ -52,18 +52,6 @@ defmodule Construct.Cast do iex> make(%{user: %{name: :string, age: {:integer, default: 21}}}, %{"user" => %{"name" => "john"}}) {:ok, %{user: %{name: "john", age: 21}}} - Options: - - * `make_map` — return result as map instead of structure, defaults to false; - * `empty_values` — list of terms indicates empty values, defaults to []. - - Example of `empty_values`: - - iex> make(%{name: {:string, []}}, %{name: ""}, empty_values: [""]) - {:error, %{name: :missing}} - - iex> make(%{name: {:string, []}}, %{name: "john"}, empty_values: ["john"]) - {:error, %{name: :missing}} """ @spec make(atom | types, map, options) :: {:ok, Construct.t | map} | {:error, term} def make(struct_or_types, params, opts \\ []) diff --git a/lib/construct/type.ex b/lib/construct/type.ex index 9b2a340..ece3462 100644 --- a/lib/construct/type.ex +++ b/lib/construct/type.ex @@ -132,7 +132,7 @@ defmodule Construct.Type do """ @spec cast(t, term, options) :: cast_ret | any - when options: [make_map: boolean] + when options: Keyword.t() def cast({:array, type}, term, opts) when is_list(term) do array(term, type, &cast/3, opts) diff --git a/test/construct/cast_test.exs b/test/construct/cast_test.exs index 2c290f3..3164617 100644 --- a/test/construct/cast_test.exs +++ b/test/construct/cast_test.exs @@ -23,10 +23,6 @@ defmodule Construct.CastTest do assert {:ok, %Construct.CastTest.Valid{a: "test"}} == Cast.make(Valid, %{a: "test"}) end - test "returns map with `make_map: true`" do - assert {:ok, %{a: "test"}} == Cast.make(Valid, %{a: "test"}, make_map: true) - end - test "accepts params as keyword list" do assert {:ok, %Valid{a: "test"}} == Cast.make(Valid, [a: "test"]) end diff --git a/test/integration/build_test.exs b/test/integration/build_test.exs index 03a4e39..ad5a38b 100644 --- a/test/integration/build_test.exs +++ b/test/integration/build_test.exs @@ -21,23 +21,6 @@ defmodule Construct.Integration.BuildTest do end) end - test "pass `empty_values` to use/2" do - module = create_construct [empty_values: ["empty_val"]] do - field :a - end - - assert {:ok, %{a: ""}} = make(module, %{a: ""}) - assert {:error, %{a: :missing}} == make(module, %{a: "empty_val"}) - end - - test "pass `make_map` to use/2" do - module = create_construct [make_map: true] do - field :a - end - - assert {:ok, %{a: ""}} == make(module, %{a: ""}) - end - test "include other structure" do include1_module = create_construct do field :a, :string, default: nil @@ -74,13 +57,15 @@ defmodule Construct.Integration.BuildTest do end end + module_name = name(module) + assert {:ok, root = %{parent: parent = %{nested_struct: nested = %{SOME_OF_1: some = %{c: "test"}}}}} = make(module, %{parent: %{nested_struct: %{SOME_OF_1: %{c: "test"}}}}) - assert Construct.Integration.BuildTest_67 == root.__struct__ - assert Construct.Integration.BuildTest_67.Parent == parent.__struct__ - assert Construct.Integration.BuildTest_67.Parent.NestedStruct == nested.__struct__ - assert Construct.Integration.BuildTest_67.Parent.NestedStruct.SOME_OF1 == some.__struct__ + assert Module.concat([module_name]) == root.__struct__ + assert Module.concat([module_name, Parent]) == parent.__struct__ + assert Module.concat([module_name, Parent, NestedStruct]) == nested.__struct__ + assert Module.concat([module_name, Parent, NestedStruct, SOME_OF1]) == some.__struct__ end test "raise when try to use non-atom field name" do diff --git a/test/integration/make_test.exs b/test/integration/make_test.exs index e13c659..25ead19 100644 --- a/test/integration/make_test.exs +++ b/test/integration/make_test.exs @@ -367,38 +367,6 @@ defmodule Construct.Integration.MakeTest do == make(module2, %{nested: [1]}, opts) end - test "make with `empty_values` option" do - opts = [empty_values: [nil, "", "test", 1.42]] - - module = create_construct do - field :key, :string - end - - assert {:error, %{key: :missing}} = make(module, %{key: nil}, opts) - assert {:error, %{key: :missing}} = make(module, %{key: ""}, opts) - assert {:error, %{key: :missing}} = make(module, %{key: "test"}, opts) - assert {:error, %{key: :missing}} = make(module, %{key: 1.42}, opts) - assert {:ok, %{key: "qwe"}} = make(module, %{key: "qwe"}, opts) - end - - test "make with `make_map: false` option" do - module = create_construct do - field :key - end - - assert {:ok, structure} = make(module, %{key: "test"}, make_map: false) - assert Map.has_key?(structure, :__struct__) - end - - test "make with `make_map: true` option" do - module = create_construct do - field :key - end - - assert {:ok, structure} = make(module, %{key: "test"}, make_map: true) - refute Map.has_key?(structure, :__struct__) - end - test "structure with `include`" do include1_module = create_construct do field :a @@ -472,7 +440,7 @@ defmodule Construct.Integration.MakeTest do include = name(include_module) - assert_raise(Construct.DefinitionError, "field :c in :only option doesn't exist in Construct.Integration.MakeTest_468", fn -> + assert_raise(Construct.DefinitionError, "field :c in :only option doesn't exist in #{inspect(include)}", fn -> create_construct do include include, only: [:c] end From f66051f0ac767f5317cd7d871e95dbe706aa4c4d Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Thu, 26 May 2022 13:50:42 +0300 Subject: [PATCH 07/36] elixir 1.10 in yaml cast as 1.1, specify 1.10.4 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f678538..e23fafd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - otp: 22.3 elixir: 1.11 - otp: 21.3 - elixir: 1.10 + elixir: 1.10.4 env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} MIX_ENV: test From ca27a73ce766893f84a6d39f540d001c733f0fc7 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Thu, 26 May 2022 13:53:03 +0300 Subject: [PATCH 08/36] fix enforce_keys values for newest elixir versions --- lib/construct/compiler.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/construct/compiler.ex b/lib/construct/compiler.ex index e927a5c..75a974b 100644 --- a/lib/construct/compiler.ex +++ b/lib/construct/compiler.ex @@ -116,7 +116,7 @@ defmodule Construct.Compiler do enforce_keys = Keyword.get(@construct_opts, :enforce_keys, true) if enforce_keys do - @enforce_keys unquote(enforce_fields) + @enforce_keys Enum.uniq(unquote(enforce_fields)) end defstruct unquote(Macro.escape(fields)) From 99f055df1cae8137282a5d91fc1e0a412497e4a9 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Thu, 26 May 2022 14:00:42 +0300 Subject: [PATCH 09/36] remove travis badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c262622..51b21ef 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Construct [![Build Status](https://img.shields.io/travis/ExpressApp/construct.svg)](https://travis-ci.org/ExpressApp/construct) [![Hex.pm](https://img.shields.io/hexpm/v/construct.svg)](https://hex.pm/packages/construct) +# Construct [![Hex.pm](https://img.shields.io/hexpm/v/construct.svg)](https://hex.pm/packages/construct) --- From 7416b149d31b54ec9d3b899424f0a1431391637f Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Thu, 26 May 2022 14:09:44 +0300 Subject: [PATCH 10/36] reverse omit_default option --- lib/construct/hooks/omit_default.ex | 6 +++++- mix.lock | 3 ++- test/integration/hooks/omit_default_test.exs | 12 ++++++------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/construct/hooks/omit_default.ex b/lib/construct/hooks/omit_default.ex index 673b780..5a646f6 100644 --- a/lib/construct/hooks/omit_default.ex +++ b/lib/construct/hooks/omit_default.ex @@ -3,7 +3,7 @@ defmodule Construct.Hooks.OmitDefault do quote do structure_compile_hook :post do @omits Enum.reduce(@fields, [], fn({name, _type, opts}, acc) -> - if Keyword.get(opts, :omit_default) == true do + if Keyword.get(opts, :omit_default, true) do [{name, Keyword.get(opts, :default)} | acc] else acc @@ -24,6 +24,10 @@ defmodule Construct.Hooks.OmitDefault do end end + def __omits__ do + @omits + end + defoverridable make: 2 end end diff --git a/mix.lock b/mix.lock index c36206e..032fde0 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"}, + "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, @@ -20,5 +20,6 @@ "nimble_parsec": {:hex, :nimble_parsec, "1.2.1", "264fc6864936b59fedb3ceb89998c64e9bb91945faf1eb115d349b96913cc2ef", [:mix], [], "hexpm", "23c31d0ec38c97bf9adde35bc91bc8e1181ea5202881f48a192f4aa2d2cf4d59"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/test/integration/hooks/omit_default_test.exs b/test/integration/hooks/omit_default_test.exs index 9b10095..827e2dd 100644 --- a/test/integration/hooks/omit_default_test.exs +++ b/test/integration/hooks/omit_default_test.exs @@ -9,17 +9,17 @@ defmodule Construct.Integration.Hooks.OmitDefaultTest do structure do field :a - field :b, [omit_default: true] do + field :b do field :ba, :string, default: "!" end - field :d do - field :da, :string, default: nil, omit_default: true + field :d, [omit_default: false] do + field :da, :string, default: nil end - field :c, [omit_default: true] do - field :ca, :string, default: nil - field :cb, :string, default: nil, omit_default: true + field :c do + field :ca, :string, default: nil, omit_default: false + field :cb, :string, default: nil end end end From df6f87b87d2d0e3fbc23b071dd3c32573ace776f Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Sat, 12 Nov 2022 11:04:07 +0300 Subject: [PATCH 11/36] fix typespecs generation based on default values --- lib/construct/compiler.ex | 22 +++++++++------- .../compiler/ast/{ast_types.ex => types.ex} | 8 ++++-- test/construct/compiler/ast/types_test.exs | 26 +++++++++++++++++++ test/integration/build_test.exs | 12 +++++++++ 4 files changed, 57 insertions(+), 11 deletions(-) rename lib/construct/compiler/ast/{ast_types.ex => types.ex} (95%) create mode 100644 test/construct/compiler/ast/types_test.exs diff --git a/lib/construct/compiler.ex b/lib/construct/compiler.ex index 75a974b..53b7535 100644 --- a/lib/construct/compiler.ex +++ b/lib/construct/compiler.ex @@ -69,7 +69,7 @@ defmodule Construct.Compiler do Module.eval_quoted(__ENV__, AST.block([ Compiler.define_struct(@construct_fields, @construct_fields_enforce), Compiler.define_construct_functions(__ENV__, @fields), - Compiler.define_typespec(@fields), + Compiler.define_typespec(__ENV__, @fields), ])) Module.eval_quoted(__ENV__, AST.block( @@ -134,16 +134,20 @@ defmodule Construct.Compiler do end end - def define_typespec(fields) do - typespecs = - Enum.map(fields, fn({name, type, opts}) -> - {name, AST.spec_type(type, opts)} - end) + def define_typespec(env, fields) do + if Module.defines_type?(env.module, {:t, 0}) do + :ok + else + typespecs = + Enum.map(fields, fn({name, type, opts}) -> + {name, AST.spec_type(type, opts)} + end) - modulespec = AST.spec_struct(typespecs) + modulespec = AST.spec_struct(typespecs) - quote do - @type t :: unquote(modulespec) + quote do + @type t :: unquote(modulespec) + end end end diff --git a/lib/construct/compiler/ast/ast_types.ex b/lib/construct/compiler/ast/types.ex similarity index 95% rename from lib/construct/compiler/ast/ast_types.ex rename to lib/construct/compiler/ast/types.ex index 6658fca..828127a 100644 --- a/lib/construct/compiler/ast/ast_types.ex +++ b/lib/construct/compiler/ast/types.ex @@ -156,8 +156,12 @@ defmodule Construct.Compiler.AST.Types do end def typeof(term) when is_atom(term) do - quote do - unquote(term).t() + if Construct.Compiler.construct_module?(term) do + quote do + unquote(term).t() + end + else + {:atom, [], []} end end diff --git a/test/construct/compiler/ast/types_test.exs b/test/construct/compiler/ast/types_test.exs new file mode 100644 index 0000000..e761122 --- /dev/null +++ b/test/construct/compiler/ast/types_test.exs @@ -0,0 +1,26 @@ +defmodule Construct.Compiler.AST.TypesTest do + use ExUnit.Case + + alias Construct.Compiler.AST.Types + + defmodule TestType do + use Construct do + field :a + end + end + + defmodule TestModule do + end + + describe "#typeof" do + test "for custom types" do + assert "Construct.Compiler.AST.TypesTest.TestType.t()" == typeof(TestType) + assert "atom()" == typeof(TestModule) + assert "atom()" == typeof(:ok) + end + end + + defp typeof(term) do + term |> Types.typeof() |> Macro.to_string() + end +end diff --git a/test/integration/build_test.exs b/test/integration/build_test.exs index ad5a38b..779e84a 100644 --- a/test/integration/build_test.exs +++ b/test/integration/build_test.exs @@ -68,6 +68,18 @@ defmodule Construct.Integration.BuildTest do assert Module.concat([module_name, Parent, NestedStruct, SOME_OF1]) == some.__struct__ end + test "able to define our own typespec" do + create_module do + @type t :: %__MODULE__{ + module: module() | term() + } + + use Construct do + field :module, :any, default: String + end + end + end + test "raise when try to use non-atom field name" do assert_raise(Construct.DefinitionError, ~s(expected atom for field name, got `"key"`), fn -> create_construct do From 7aa8adb37417d0c496d6d81ded111ad58b0aa3dc Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Sat, 12 Nov 2022 14:04:25 +0300 Subject: [PATCH 12/36] fix typespec generation for builtin types --- lib/construct/compiler/ast/types.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/construct/compiler/ast/types.ex b/lib/construct/compiler/ast/types.ex index 828127a..faad9e1 100644 --- a/lib/construct/compiler/ast/types.ex +++ b/lib/construct/compiler/ast/types.ex @@ -82,7 +82,7 @@ defmodule Construct.Compiler.AST.Types do end def spec(type) when type in @builtin do - type + {type, [], []} end def spec(type) when is_atom(type) do From a30b7d4dda120290956919cf84df9616765ef5e1 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Sat, 12 Nov 2022 14:29:31 +0300 Subject: [PATCH 13/36] fix typespec generation for lists with default --- lib/construct/compiler/ast.ex | 23 ++++++++++++++++------- test/integration/cycle_deps_test.exs | 2 +- test/support/constructs/post.ex | 2 +- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/lib/construct/compiler/ast.ex b/lib/construct/compiler/ast.ex index 86ffb7a..0d6a575 100644 --- a/lib/construct/compiler/ast.ex +++ b/lib/construct/compiler/ast.ex @@ -22,16 +22,25 @@ defmodule Construct.Compiler.AST do case Keyword.fetch(opts, :default) do {:ok, default} -> - typeof_default = Construct.Compiler.AST.Types.typeof(default) - - if type == typeof_default do - type - else - quote do: unquote(type) | unquote(typeof_default) - end + spec_type_default(type, default) :error -> type end end + + defp spec_type_default(type, default) do + case {type, Construct.Compiler.AST.Types.typeof(default)} do + {term, term} -> + type + + {{:list, _, _} = type, {:list, [], []}} -> + type + + {type, typeof_default} -> + quote do + unquote(type) | unquote(typeof_default) + end + end + end end diff --git a/test/integration/cycle_deps_test.exs b/test/integration/cycle_deps_test.exs index 39d4d9b..f335a95 100644 --- a/test/integration/cycle_deps_test.exs +++ b/test/integration/cycle_deps_test.exs @@ -12,7 +12,7 @@ defmodule Construct.Integration.CycleDepsTest do test "makes cross-dependent constructs properly" do assert {:ok, %Post{id: 1, comments: [%Comment{id: 2, post: nil}, - %Comment{id: 3, post: %Post{comments: nil, id: 1}}]}} + %Comment{id: 3, post: %Post{comments: [], id: 1}}]}} == Post.make(%{id: 1, comments: [%{id: 2}, %{id: 3, post: %{id: 1}}]}) end end diff --git a/test/support/constructs/post.ex b/test/support/constructs/post.ex index 2050081..6bd92fe 100644 --- a/test/support/constructs/post.ex +++ b/test/support/constructs/post.ex @@ -4,6 +4,6 @@ defmodule Post do structure do field :id, :integer - field :comments, {:array, Comment}, default: nil + field :comments, {:array, Comment}, default: [] end end From 9ac2ffdde26c0254b35137ec17213b36042f35fe Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Mon, 14 Nov 2022 10:22:21 +0300 Subject: [PATCH 14/36] optimize cast to remove ensure_loaded? --- lib/construct/type.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/construct/type.ex b/lib/construct/type.ex index ece3462..f7d57e9 100644 --- a/lib/construct/type.ex +++ b/lib/construct/type.ex @@ -149,7 +149,7 @@ defmodule Construct.Type do def cast(type, term, opts) when is_atom(type) do cond do not primitive?(type) -> - if Code.ensure_loaded?(type) && function_exported?(type, :cast, 2) do + if function_exported?(type, :cast, 2) do type.cast(term, opts) else type.cast(term) From baefaf853bd963a070741d22cd195d2b55d91fba Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Mon, 14 Nov 2022 10:22:38 +0300 Subject: [PATCH 15/36] optimize cast to use Map.new instead of Enum.into --- lib/construct/cast.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/construct/cast.ex b/lib/construct/cast.ex index 6a01bc7..068fa8b 100644 --- a/lib/construct/cast.ex +++ b/lib/construct/cast.ex @@ -131,7 +131,7 @@ defmodule Construct.Cast do end) |> case do nil -> params - list -> Enum.into(list, %{}) + list -> Map.new(list) end end @@ -144,11 +144,11 @@ defmodule Construct.Cast do end defp convert_types(types) when is_list(types) do - Enum.into(types, %{}) + Map.new(types) end - defp convert_types(invalid_types) do - raise Construct.Error, "expected types to be a {key, value} structure, got: #{inspect(invalid_types)}" + defp convert_types(types) do + raise Construct.Error, "expected types to be a {key, value} structure, got: #{inspect(types)}" end defp process_param(key, params, types, opts, {changes, errors, valid?}) do @@ -158,6 +158,7 @@ defmodule Construct.Cast do case cast_field(param_key, type, type_opts, params, opts) do {:ok, value} -> {Map.put(changes, key, value), errors, valid?} + {:error, reason} -> {changes, Map.put(errors, key, reason), false} end @@ -189,7 +190,6 @@ defmodule Construct.Cast do else {:ok, make_default_value(default_value)} end - end end From 91e1f0a47a3ab33014cdc882f48af7919dc60517 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Mon, 14 Nov 2022 10:27:04 +0300 Subject: [PATCH 16/36] add new versions to ci --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e23fafd..ac3bd26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,10 +14,12 @@ jobs: fail-fast: false matrix: include: - - otp: 24.3 - elixir: 1.13 + - otp: 25 + elixir: 1.14 coverage: true lint: true + - otp: 24.3 + elixir: 1.13 - otp: 23.3 elixir: 1.12 - otp: 22.3 From 1ec465e123af717797efb30da6dabd51670f19a4 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Mon, 14 Nov 2022 10:33:39 +0300 Subject: [PATCH 17/36] update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f470d0c..9b54dd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ * Remove `make_map` option * Remove `empty_values` option +* Bug fixes + * Fix typespecs generation + ## v2.1.10 * Enhancements From 366af581e8e045ade30245425f3306e4fb9d84a8 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Mon, 14 Nov 2022 10:44:43 +0300 Subject: [PATCH 18/36] update bench script --- bench/struct_bench.exs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bench/struct_bench.exs b/bench/struct_bench.exs index 4e56ac6..8dc5346 100644 --- a/bench/struct_bench.exs +++ b/bench/struct_bench.exs @@ -19,8 +19,11 @@ end Benchee.run( %{ - "make" => fn -> {:ok, _} = Example.make(%{a: "test", b: 1.42, c: %{a: 0, b: 42}, d: %{e: "embeds"}}) end, + "make" => fn -> + {:ok, _} = Example.make(%{a: "test", b: 1.42, c: %{a: 0, b: 42}, d: %{e: "embeds"}}) + end, }, - time: 10, - memory_time: 2 + time: 3, + memory_time: 3, + reduction_time: 3 ) From 2ba15d4f2e1d34f11f05ba3bb1e25c723fec3f1f Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Mon, 14 Nov 2022 10:45:07 +0300 Subject: [PATCH 19/36] optimize cast to move error_values option higher --- lib/construct/cast.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/construct/cast.ex b/lib/construct/cast.ex index 068fa8b..89db308 100644 --- a/lib/construct/cast.ex +++ b/lib/construct/cast.ex @@ -99,8 +99,9 @@ defmodule Construct.Cast do params = convert_params(params) types = convert_types(types) permitted = Map.keys(types) + error_values = Keyword.get(opts, :error_values, false) - case Enum.reduce(permitted, {%{}, %{}, true}, &process_param(&1, params, types, opts, &2)) do + case Enum.reduce(permitted, {%{}, %{}, true}, &process_param(&1, params, types, error_values, opts, &2)) do {changes, _errors, true} -> {:ok, changes} {_changes, errors, false} -> {:error, errors} end @@ -151,11 +152,11 @@ defmodule Construct.Cast do raise Construct.Error, "expected types to be a {key, value} structure, got: #{inspect(types)}" end - defp process_param(key, params, types, opts, {changes, errors, valid?}) do + defp process_param(key, params, types, error_values, opts, {changes, errors, valid?}) do param_key = Atom.to_string(key) {type, type_opts} = type!(key, types) - case cast_field(param_key, type, type_opts, params, opts) do + case cast_field(param_key, type, type_opts, params, error_values, opts) do {:ok, value} -> {Map.put(changes, key, value), errors, valid?} @@ -173,9 +174,8 @@ defmodule Construct.Cast do end end - defp cast_field(param_key, type, type_opts, params, opts) do + defp cast_field(param_key, type, type_opts, params, error_values, opts) do default_value = Keyword.get(type_opts, :default, @default_value) - error_values = Keyword.get(opts, :error_values, false) case params do %{^param_key => value} when default_value != @default_value and value == default_value -> From 5f328d729e5c22b37e1bd2c76478b9173cb4c5c3 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Thu, 17 Nov 2022 22:48:32 +0300 Subject: [PATCH 20/36] release 3.0.0 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index d296afd..8a9946c 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Construct.Mixfile do def project do [ app: :construct, - version: "3.0.0-rc.0", + version: "3.0.0", elixir: "~> 1.9", deps: deps(), elixirc_paths: elixirc_paths(Mix.env), From 38acc7c1eb0c6cdaa11568f7e2393dd93eb8841a Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Thu, 17 Nov 2022 22:53:49 +0300 Subject: [PATCH 21/36] update ex_doc dep --- mix.exs | 1 - mix.lock | 11 +++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/mix.exs b/mix.exs index 8a9946c..4debda6 100644 --- a/mix.exs +++ b/mix.exs @@ -40,7 +40,6 @@ defmodule Construct.Mixfile do {:decimal, "~> 1.6 or ~> 2.0", only: [:dev, :test]}, {:benchee, "~> 1.0", only: [:dev, :test]}, {:dialyxir, "~> 1.1.0", only: [:dev, :test], runtime: false}, - {:earmark, "~> 1.4", only: :dev}, {:ex_doc, "~> 0.28", only: :dev}, {:jason, "~> 1.3", only: :test}, {:excoveralls, "~> 0.14", only: :test} diff --git a/mix.lock b/mix.lock index 032fde0..c5c1ee1 100644 --- a/mix.lock +++ b/mix.lock @@ -4,20 +4,19 @@ "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, - "earmark": {:hex, :earmark, "1.4.19", "3854a17305c880cc46305af15fb1630568d23a709aba21aaa996ced082fc29d7", [:mix], [{:earmark_parser, ">= 1.4.18", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "d5a8c9f9e37159a8fdd3ea8437fb4e229eaf56d5129b9a011dc4780a4872079d"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.28.0", "7eaf526dd8c80ae8c04d52ac8801594426ae322b52a6156cd038f30bafa8226f", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e55cdadf69a5d1f4cfd8477122ebac5e1fadd433a8c1022dafc5025e48db0131"}, + "ex_doc": {:hex, :ex_doc, "0.29.0", "4a1cb903ce746aceef9c1f9ae8a6c12b742a5461e6959b9d3b24d813ffbea146", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "f096adb8bbca677d35d278223361c7792d496b3fc0d0224c9d4bc2f651af5db1"}, "excoveralls": {:hex, :excoveralls, "0.14.5", "5c685449596e962c779adc8f4fb0b4de3a5b291c6121097572a3aa5400c386d3", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9b4a9bf10e9a6e48b94159e13b4b8a1b05400f17ac16cc363ed8734f26e1f4e"}, "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, - "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.2.1", "264fc6864936b59fedb3ceb89998c64e9bb91945faf1eb115d349b96913cc2ef", [:mix], [], "hexpm", "23c31d0ec38c97bf9adde35bc91bc8e1181ea5202881f48a192f4aa2d2cf4d59"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, From 898142d2da6fa01740f6902fa1433ab486018e89 Mon Sep 17 00:00:00 2001 From: yunmi Date: Tue, 14 Mar 2023 19:38:12 +0300 Subject: [PATCH 22/36] added new types and prettified docs New types include: - Construct.Types.CommaList - Construct.Types.Enum - Construct.Types.UUID --- lib/construct/hooks/map.ex | 2 ++ lib/construct/hooks/omit_default.ex | 2 ++ lib/construct/type.ex | 4 +++ lib/construct/type_c.ex | 6 +++++ lib/construct/types/comma_list.ex | 41 +++++++++++++++++++++++++++++ lib/construct/types/enum.ex | 32 ++++++++++++++++++++++ lib/construct/types/uuid.ex | 23 ++++++++++++++++ mix.exs | 9 ++++++- test/construct/cast_test.exs | 1 + test/construct/type_test.exs | 1 + test/construct/types/uuid_test.exs | 16 +++++++++++ test/integration/make_test.exs | 22 +++++++++------- test/support/types.ex | 21 --------------- 13 files changed, 149 insertions(+), 31 deletions(-) create mode 100644 lib/construct/types/comma_list.ex create mode 100644 lib/construct/types/enum.ex create mode 100644 lib/construct/types/uuid.ex create mode 100644 test/construct/types/uuid_test.exs diff --git a/lib/construct/hooks/map.ex b/lib/construct/hooks/map.ex index 725f0df..b5288b0 100644 --- a/lib/construct/hooks/map.ex +++ b/lib/construct/hooks/map.ex @@ -1,4 +1,6 @@ defmodule Construct.Hooks.Map do + @moduledoc false + defmacro __using__(_opts \\ []) do quote do structure_compile_hook :post do diff --git a/lib/construct/hooks/omit_default.ex b/lib/construct/hooks/omit_default.ex index 5a646f6..dd58c6f 100644 --- a/lib/construct/hooks/omit_default.ex +++ b/lib/construct/hooks/omit_default.ex @@ -1,4 +1,6 @@ defmodule Construct.Hooks.OmitDefault do + @moduledoc false + defmacro __using__(_opts \\ []) do quote do structure_compile_hook :post do diff --git a/lib/construct/type.ex b/lib/construct/type.ex index f7d57e9..607e0bf 100644 --- a/lib/construct/type.ex +++ b/lib/construct/type.ex @@ -130,6 +130,10 @@ defmodule Construct.Type do iex> cast(:string, [1, 2, 3]) :error + iex> cast({Construct.Types.Enum, [:a, :b, :c]}, :a) + {:ok, :a} + iex> cast({Construct.Types.Enum, [:a, :b, :c]}, :d) + {:error, passed_value: :d, valid_values: [:a, :b, :c]} """ @spec cast(t, term, options) :: cast_ret | any when options: Keyword.t() diff --git a/lib/construct/type_c.ex b/lib/construct/type_c.ex index 755694c..f794099 100644 --- a/lib/construct/type_c.ex +++ b/lib/construct/type_c.ex @@ -1,3 +1,9 @@ defmodule Construct.TypeC do + @moduledoc """ + Provides a way to create parametrized types. + + See `Construct.Types.Enum` implementation for more information. + """ + @callback castc(term, arg :: term) :: Construct.Type.cast_ret() end diff --git a/lib/construct/types/comma_list.ex b/lib/construct/types/comma_list.ex new file mode 100644 index 0000000..e249bca --- /dev/null +++ b/lib/construct/types/comma_list.ex @@ -0,0 +1,41 @@ +defmodule Construct.Types.CommaList do + @moduledoc """ + Extracts list of separated by comma values from string. + + You can use it alone, just for splitting values: + + defmodule Structure do + use Construct do + field :values, Construct.Types.CommaList + end + end + + iex> Structure.make!(values: "foo,bar,baz,42") + %Structure{values: ["foo", "bar", "baz", "42"]} + + iex> Structure.make!(values: ["foo", 42]) + %Structure{values: ["foo", 42]} + + Also you can compose it with other types: + + defmodule UserInfoRequest do + use Construct do + field :user_ids, [Construct.Types.CommaList, {:array, :integer}] + end + end + + iex> UserInfoRequest.make!(%{user_ids: "1,2,42"}) + %UserInfoRequest{user_ids: [1, 2, 42]} + + iex> UserInfoRequest.make(%{user_ids: "1,foo"}) + {:error, %{user_ids: :invalid}} + """ + + @behaviour Construct.Type + + @impl true + def cast(""), do: {:ok, []} + def cast(v) when is_binary(v), do: {:ok, String.split(v, ",")} + def cast(v) when is_list(v), do: {:ok, v} + def cast(_), do: :error +end diff --git a/lib/construct/types/enum.ex b/lib/construct/types/enum.ex new file mode 100644 index 0000000..f08d94c --- /dev/null +++ b/lib/construct/types/enum.ex @@ -0,0 +1,32 @@ +defmodule Construct.Types.Enum do + @moduledoc """ + Implements an abstract enum type. + + ## Usage + + defmodule MyApp.Order do + use Construct do + field :type, {Construct.Types.Enum, [:delivery, :pickup]} + end + end + + Then you can validate that `:type` field accepts only specified + values. + + iex> MyApp.Order.make(%{type: :delivery}) + {:ok, %MyApp.Order{type: :delivery}} + iex> MyApp.Order.make(%{type: :other}) + {:error, %{type: passed_value: :other, valid_values: [:delivery, :pickup]}} + """ + + @behaviour Construct.TypeC + + @impl true + def castc(value, variants) when is_list(variants) do + if value in variants do + {:ok, value} + else + {:error, passed_value: value, valid_values: variants} + end + end +end diff --git a/lib/construct/types/uuid.ex b/lib/construct/types/uuid.ex new file mode 100644 index 0000000..b5f815b --- /dev/null +++ b/lib/construct/types/uuid.ex @@ -0,0 +1,23 @@ +defmodule Construct.Types.UUID do + @moduledoc """ + Checks that provided binary is UUID-like string: + + defmodule Structure do + use Construct do + field :value, Construct.Types.UUID + end + end + + iex> Structure.make!(value: "fd4ddf80-a7d9-4af8-b46c-26fc4566d92c") + %Structure{value: "fd4ddf80-a7d9-4af8-b46c-26fc4566d92c"} + + iex> Structure.make(value: "invalid") + {:error, %{value: :invalid}} + """ + + @behaviour Construct.Type + + @impl true + def cast(<<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = v), do: {:ok, v} + def cast(_), do: :error +end diff --git a/mix.exs b/mix.exs index 4debda6..006bbe5 100644 --- a/mix.exs +++ b/mix.exs @@ -63,7 +63,14 @@ defmodule Construct.Mixfile do [ main: "readme", source_url: "https://github.com/ExpressApp/construct", - extras: ["README.md"] + extras: ["README.md"], + groups_for_modules: [ + "Provided types": [ + Construct.Types.CommaList, + Construct.Types.Enum, + Construct.Types.UUID + ] + ] ] end end diff --git a/test/construct/cast_test.exs b/test/construct/cast_test.exs index 3164617..d3132ea 100644 --- a/test/construct/cast_test.exs +++ b/test/construct/cast_test.exs @@ -2,6 +2,7 @@ defmodule Construct.CastTest do use ExUnit.Case alias Construct.Cast + alias Construct.Types.CommaList doctest Construct.Cast, import: true diff --git a/test/construct/type_test.exs b/test/construct/type_test.exs index 1de617c..1825127 100644 --- a/test/construct/type_test.exs +++ b/test/construct/type_test.exs @@ -2,6 +2,7 @@ defmodule Construct.TypeTest do use Construct.TestCase alias Construct.Type + alias Construct.Types.CommaList doctest Construct.Type, import: true diff --git a/test/construct/types/uuid_test.exs b/test/construct/types/uuid_test.exs new file mode 100644 index 0000000..333eece --- /dev/null +++ b/test/construct/types/uuid_test.exs @@ -0,0 +1,16 @@ +defmodule Construct.Types.UUIDTest do + use Construct.TestCase + + alias Construct.Types.UUID + + describe "#cast" do + test "oks on a valid UUID" do + uuid = "fd4ddf80-a7d9-4af8-b46c-26fc4566d92c" + assert {:ok, ^uuid} = Construct.Type.cast(UUID, uuid) + end + + test "returns an error on an invalid UUID" do + assert :error = Construct.Type.cast(UUID, "invalid") + end + end +end diff --git a/test/integration/make_test.exs b/test/integration/make_test.exs index 25ead19..fb421f4 100644 --- a/test/integration/make_test.exs +++ b/test/integration/make_test.exs @@ -1,6 +1,10 @@ defmodule Construct.Integration.MakeTest do use Construct.TestCase + # For some reason it keeps emitting a warning about an unused alias + # while it's actually used. + alias Construct.Types.CommaList, warn: false + test "with simple stupid params" do module = create_construct do field :key @@ -84,23 +88,23 @@ defmodule Construct.Integration.MakeTest do test "field with type `[t, {t, ...}]`" do module = create_construct do - field :key, [:string, {EnumT, ~w(A B C)}] + field :key, [:string, {Construct.Types.Enum, ~w(A B C)}] end - assert {:ok, %{key: "A"}} = make(module, %{key: "a"}) - assert {:error, %{key: :invalid}} = make(module, %{key: "d"}) + assert {:ok, %{key: "A"}} = make(module, %{key: "A"}) + assert {:error, %{key: [passed_value: "D", valid_values: ["A", "B", "C"]]}} = make(module, %{key: "D"}) assert {:error, %{key: :invalid}} = make(module, %{key: 123}) assert {:error, %{key: :missing}} = make(module, %{}) end test "field with type `[t, {:array, {t, ...}}]`" do module = create_construct do - field :key, [CommaList, {:array, {EnumT, ~w(A B C)}}] + field :key, [CommaList, {:array, {Construct.Types.Enum, ~w(A B C)}}] end - assert {:ok, %{key: ["A"]}} = make(module, %{key: "a"}) - assert {:ok, %{key: ["A", "C", "B"]}} = make(module, %{key: "a,c,b"}) - assert {:error, %{key: :invalid}} = make(module, %{key: "a,d"}) + assert {:ok, %{key: ["A"]}} = make(module, %{key: "A"}) + assert {:ok, %{key: ["A", "C", "B"]}} = make(module, %{key: "A,C,B"}) + assert {:error, %{key: [passed_value: "D", valid_values: ["A", "B", "C"]]}} = make(module, %{key: "A,D"}) assert {:error, %{key: :invalid}} = make(module, %{key: 123}) assert {:error, %{key: :missing}} = make(module, %{}) end @@ -295,7 +299,7 @@ defmodule Construct.Integration.MakeTest do end assert {:error, - %{nested: %{error: :missing, value: nil, expect: "array of Construct.Integration.MakeTest_287 is expected"}}} + %{nested: %{error: :missing, value: nil, expect: "array of Construct.Integration.MakeTest_291 is expected"}}} == make(module2, %{}, opts) assert {:error, @@ -360,7 +364,7 @@ defmodule Construct.Integration.MakeTest do field :nested, {:array, module1_type} end - assert {:error, %{nested: %{error: :missing, value: nil, expect: "array of Construct.Integration.MakeTest_352 is expected"}}} + assert {:error, %{nested: %{error: :missing, value: nil, expect: "array of Construct.Integration.MakeTest_356 is expected"}}} == make(module2, %{}, opts) assert {:error, %{nested: [%{error: %{key: %{error: :missing, expect: "Comment is expected", value: nil}}, index: 0}]}} diff --git a/test/support/types.ex b/test/support/types.ex index 9d1c54d..b432662 100644 --- a/test/support/types.ex +++ b/test/support/types.ex @@ -8,13 +8,6 @@ defmodule EctoType do def cast(_), do: :error end -defmodule CommaList do - def cast(""), do: {:ok, []} - def cast(v) when is_binary(v), do: {:ok, String.split(v, ",")} - def cast(v) when is_list(v), do: {:ok, v} - def cast(_), do: :error -end - defmodule CustomTypeInvalid do def cast(_), do: :invalid_ret end @@ -28,17 +21,3 @@ defmodule Nilable do def castc(nil, _), do: {:ok, nil} def castc(val, type), do: Construct.Type.cast(type, val) end - -defmodule EnumT do - @behaviour Construct.TypeC - - def castc(val, enums) do - val = String.upcase(val) - - if val in enums do - {:ok, val} - else - :error - end - end -end From 67bb6f272e2af39bdfdba00e98fada411a291562 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Tue, 11 Apr 2023 12:43:04 +0300 Subject: [PATCH 23/36] add changelog for 3.0.1 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b54dd2..c4b0763 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Changelog +## v3.0.1 + +* Enhancements + * Add `Construct.Types` with `CommaList`, `Enum` and `UUID` + ## v3.0.0 * Enhancements From 21ecaf58fb90e97980e73862fddc8f58ce9830f3 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Tue, 11 Apr 2023 12:43:30 +0300 Subject: [PATCH 24/36] release 3.0.1 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 006bbe5..c460b95 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Construct.Mixfile do def project do [ app: :construct, - version: "3.0.0", + version: "3.0.1", elixir: "~> 1.9", deps: deps(), elixirc_paths: elixirc_paths(Mix.env), From ac3340e39e237bd9ac3ac83d04fb57c4b951445e Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Tue, 14 Jan 2025 08:27:20 +0300 Subject: [PATCH 25/36] prepare for elixir 1.18 (#47) * update ci.yml * add support for new decimal * fix warnings for elixir 1.18 * remove coverage task * use elixir 1.18.1 in ci --- .github/workflows/ci.yml | 27 +++++++++------------- lib/construct/compiler.ex | 18 +++++++-------- lib/construct/type.ex | 6 ++++- mix.exs | 14 +++++------- mix.lock | 31 +++++++++----------------- test/construct/type_test.exs | 4 ++-- test/integration/compile_hook_test.exs | 8 +++++-- 7 files changed, 49 insertions(+), 59 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac3bd26..65e0fb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,26 +9,21 @@ on: jobs: test: name: Test (Elixir ${{matrix.elixir}} | Erlang/OTP ${{matrix.otp}}) - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + MIX_ENV: test strategy: fail-fast: false matrix: include: - - otp: 25 - elixir: 1.14 - coverage: true - lint: true - - otp: 24.3 - elixir: 1.13 - - otp: 23.3 - elixir: 1.12 - - otp: 22.3 - elixir: 1.11 - - otp: 21.3 - elixir: 1.10.4 - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - MIX_ENV: test + - elixir: "1.18.1" + otp: "27.1" + lint: lint + - elixir: "1.17.3" + otp: "25.0.4" + - elixir: "1.14.5" + otp: "23.3.4.20" steps: - name: Clone the repository diff --git a/lib/construct/compiler.ex b/lib/construct/compiler.ex index 53b7535..e7c7de2 100644 --- a/lib/construct/compiler.ex +++ b/lib/construct/compiler.ex @@ -51,9 +51,9 @@ defmodule Construct.Compiler do Module.put_attribute(__MODULE__, :construct_defined, true) - Module.eval_quoted(__ENV__, AST.block( + Code.eval_quoted_with_env(AST.block( Enum.reverse(Module.get_attribute(__MODULE__, :construct_compile_hook_pre)) - )) + ), [], __ENV__) try do import Construct, only: [ @@ -66,15 +66,15 @@ defmodule Construct.Compiler do :ok end - Module.eval_quoted(__ENV__, AST.block([ + Code.eval_quoted_with_env(AST.block([ Compiler.define_struct(@construct_fields, @construct_fields_enforce), Compiler.define_construct_functions(__ENV__, @fields), Compiler.define_typespec(__ENV__, @fields), - ])) + ]), [], __ENV__) - Module.eval_quoted(__ENV__, AST.block( + Code.eval_quoted_with_env(AST.block( Enum.reverse(Module.get_attribute(__MODULE__, :construct_compile_hook_post)) - )) + ), [], __ENV__) end end @@ -183,9 +183,9 @@ defmodule Construct.Compiler do defmodule module_name do use Construct - Module.eval_quoted(__ENV__, derives_ast) - Module.eval_quoted(__ENV__, definition_pre_ast) - Module.eval_quoted(__ENV__, definition_post_ast) + Code.eval_quoted_with_env(derives_ast, [], __ENV__) + Code.eval_quoted_with_env(definition_pre_ast, [], __ENV__) + Code.eval_quoted_with_env(definition_post_ast, [], __ENV__) structure do unquote(ast) diff --git a/lib/construct/type.ex b/lib/construct/type.ex index 607e0bf..0705810 100644 --- a/lib/construct/type.ex +++ b/lib/construct/type.ex @@ -218,7 +218,11 @@ defmodule Construct.Type do def cast(:reference, term) when is_reference(term), do: {:ok, term} def cast(:decimal, term) when is_binary(term) do - validate_decimal(apply(Decimal, :parse, [term])) + case apply(Decimal, :parse, [term]) do + {:ok, term} -> validate_decimal({:ok, term}) + {%{__struct__: Decimal} = term, _} -> validate_decimal({:ok, term}) + :error -> :error + end end def cast(:decimal, term) when is_integer(term) do {:ok, apply(Decimal, :new, [term])} diff --git a/mix.exs b/mix.exs index c460b95..910e9c0 100644 --- a/mix.exs +++ b/mix.exs @@ -4,8 +4,8 @@ defmodule Construct.Mixfile do def project do [ app: :construct, - version: "3.0.1", - elixir: "~> 1.9", + version: "3.1.0", + elixir: "~> 1.14", deps: deps(), elixirc_paths: elixirc_paths(Mix.env), consolidate_protocols: Mix.env() != :test, @@ -13,9 +13,6 @@ defmodule Construct.Mixfile do plt_file: {:no_warn, "_build/dialyzer.plt"} ], - # Tests - test_coverage: [tool: ExCoveralls], - # Hex description: description(), package: package(), @@ -39,10 +36,9 @@ defmodule Construct.Mixfile do [ {:decimal, "~> 1.6 or ~> 2.0", only: [:dev, :test]}, {:benchee, "~> 1.0", only: [:dev, :test]}, - {:dialyxir, "~> 1.1.0", only: [:dev, :test], runtime: false}, - {:ex_doc, "~> 0.28", only: :dev}, - {:jason, "~> 1.3", only: :test}, - {:excoveralls, "~> 0.14", only: :test} + {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, + {:ex_doc, "~> 0.35", only: :dev}, + {:jason, "~> 1.4", only: :test} ] end diff --git a/mix.lock b/mix.lock index c5c1ee1..517c865 100644 --- a/mix.lock +++ b/mix.lock @@ -1,24 +1,15 @@ %{ - "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, - "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, - "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, + "benchee": {:hex, :benchee, "1.3.1", "c786e6a76321121a44229dde3988fc772bca73ea75170a73fd5f4ddf1af95ccf", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "76224c58ea1d0391c8309a8ecbfe27d71062878f59bd41a390266bf4ac1cc56d"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.29.0", "4a1cb903ce746aceef9c1f9ae8a6c12b742a5461e6959b9d3b24d813ffbea146", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "f096adb8bbca677d35d278223361c7792d496b3fc0d0224c9d4bc2f651af5db1"}, - "excoveralls": {:hex, :excoveralls, "0.14.5", "5c685449596e962c779adc8f4fb0b4de3a5b291c6121097572a3aa5400c386d3", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9b4a9bf10e9a6e48b94159e13b4b8a1b05400f17ac16cc363ed8734f26e1f4e"}, - "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/test/construct/type_test.exs b/test/construct/type_test.exs index 1825127..75e75d7 100644 --- a/test/construct/type_test.exs +++ b/test/construct/type_test.exs @@ -136,9 +136,9 @@ defmodule Construct.TypeTest do == Type.cast(:decimal, 1) assert {:ok, Decimal.new("1")} == Type.cast(:decimal, Decimal.new("1")) - assert :error + assert {:ok, Decimal.new("NaN")} == Type.cast(:decimal, "nan") - assert :error + assert {:ok, Decimal.new("NaN")} == Type.cast(:decimal, Decimal.new("NaN")) assert :error == Type.cast(:decimal, Decimal.new("Infinity")) diff --git a/test/integration/compile_hook_test.exs b/test/integration/compile_hook_test.exs index c0e4e7d..cbaff57 100644 --- a/test/integration/compile_hook_test.exs +++ b/test/integration/compile_hook_test.exs @@ -30,8 +30,12 @@ defmodule Construct.Integration.CompileHookTest do assert {:ok, structure} = make(module, a: "string") - assert ~s({"a":"string","b":{"ba":{"baa":"test"},"bb":42},"d":{"da":{"daa":0}}}) - == Jason.encode!(structure) + assert json = Jason.encode!(structure) + assert %{ + "a" => "string", + "b" => %{"ba" => %{"baa" => "test"}, "bb" => 42}, + "d" => %{"da" => %{"daa" => 0}} + } == Jason.decode!(json) end test "compile hook pass its ast down to nested structs" do From f32d9cee196d5cdb720195f60b52439ff0918317 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Tue, 14 Jan 2025 08:32:28 +0300 Subject: [PATCH 26/36] add experimental fasten hook (#45) --- bench/struct_bench.exs | 27 ++++++++ lib/construct/compiler.ex | 2 +- lib/construct/hooks/fasten.ex | 126 ++++++++++++++++++++++++++++++++++ lib/construct/type.ex | 4 +- 4 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 lib/construct/hooks/fasten.ex diff --git a/bench/struct_bench.exs b/bench/struct_bench.exs index 8dc5346..60bc768 100644 --- a/bench/struct_bench.exs +++ b/bench/struct_bench.exs @@ -14,6 +14,29 @@ defmodule Example do field :b, :float field :c, {:map, :integer} field :d, Embedded + field :e, {:map, :integer}, default: %{} + end +end + +defmodule EmbeddedFast do + use Construct + use Construct.Hooks.Fasten + + structure do + field :e + end +end + +defmodule ExampleFast do + use Construct + use Construct.Hooks.Fasten + + structure do + field :a + field :b, :float + field :c, {:map, :integer} + field :d, EmbeddedFast + field :e, {:map, :integer}, default: %{} end end @@ -22,6 +45,10 @@ Benchee.run( "make" => fn -> {:ok, _} = Example.make(%{a: "test", b: 1.42, c: %{a: 0, b: 42}, d: %{e: "embeds"}}) end, + + "make fasten hook" => fn -> + {:ok, _} = ExampleFast.make(%{a: "test", b: 1.42, c: %{a: 0, b: 42}, d: %{e: "embeds"}}) + end, }, time: 3, memory_time: 3, diff --git a/lib/construct/compiler.ex b/lib/construct/compiler.ex index e7c7de2..49570f4 100644 --- a/lib/construct/compiler.ex +++ b/lib/construct/compiler.ex @@ -5,7 +5,7 @@ defmodule Construct.Compiler do alias Construct.Compiler.AST @registry Construct.Registry - @no_default :__construct_no_default__ + @no_default :__construct_no_default_value__ def construct_module?(module) do registered_type?(module) || ensure_compiled?(module) && function_exported?(module, :__construct__, 1) diff --git a/lib/construct/hooks/fasten.ex b/lib/construct/hooks/fasten.ex new file mode 100644 index 0000000..a7941e2 --- /dev/null +++ b/lib/construct/hooks/fasten.ex @@ -0,0 +1,126 @@ +defmodule Construct.Hooks.Fasten do + defmacro __using__(_opts \\ []) do + quote do + structure_compile_hook :post do + Module.eval_quoted(__MODULE__, Construct.Hooks.Fasten.__compile__(__MODULE__, Enum.reverse(@fields))) + + defoverridable make: 2 + end + end + end + + def __compile__(module, fields) do + cast_defs = + Enum.map(fields, fn({name, type, opts}) -> + function_name = :"__cast_#{name}__" + + {default_before_clause, default_after_clause} = + case Keyword.get(opts, :default) do + :__construct_no_default_value__ -> + clause_after = + quote do + defp unquote(function_name)(_, _) do + {:error, %{unquote(name) => :missing}} + end + end + + {[], clause_after} + + nil -> + clause_after = + quote do + defp unquote(function_name)(_, _) do + {:error, %{unquote(name) => :missing}} + end + end + + {[], clause_after} + + term -> + term = Macro.escape(term) + + clause_before = + quote do + defp unquote(function_name)(%{unquote(to_string(name)) => term}, _opts) when term == unquote(term) do + {:ok, unquote(term)} + end + + defp unquote(function_name)(%{unquote(name) => term}, _opts) when term == unquote(term) do + {:ok, unquote(term)} + end + end + + clause_after = + quote do + defp unquote(function_name)(_, _) do + {:ok, unquote(term)} + end + end + + {clause_before, clause_after} + end + + cast_clause = + quote do + defp unquote(function_name)(%{unquote(to_string(name)) => term}, opts) do + Construct.Type.cast(unquote(type), term, opts) + end + + defp unquote(function_name)(%{unquote(name) => term}, opts) do + Construct.Type.cast(unquote(type), term, opts) + end + end + + default_before_clause |> merge_blocks(cast_clause) |> merge_blocks(default_after_clause) + end) + + cast_defs = + Enum.reduce(cast_defs, {:__block__, [], []}, fn(ast, acc) -> + merge_blocks(acc, ast) + end) + + with_body = + Enum.map(fields, fn({name, _type, _opts}) -> + {name, Macro.var(name, nil)} + end) + + with_body = + quote do + {:ok, struct(unquote(module), unquote(with_body))} + end + + with_matches = + Enum.map(fields, fn({name, _type, _opts}) -> + quote do + {:ok, unquote(Macro.var(name, nil))} <- unquote(:"__cast_#{name}__")(params, opts) + end + end) + + with_ast = {:with, [], with_matches ++ [[do: with_body]]} + + make_ast = + quote do + def make(params, opts) do + unquote(with_ast) + end + end + + merge_blocks(make_ast, cast_defs) + end + + defp merge_blocks(a, b) do + {:__block__, [], block_content(a) ++ block_content(b)} + end + + defp block_content({:__block__, [], content}) do + content + end + + defp block_content({_, _, _} = expr) do + [expr] + end + + defp block_content([]) do + [] + end +end diff --git a/lib/construct/type.ex b/lib/construct/type.ex index 0705810..503f1fc 100644 --- a/lib/construct/type.ex +++ b/lib/construct/type.ex @@ -468,11 +468,9 @@ defmodule Construct.Type do {:ok, Enum.reverse(acc)} end - defp map(list, type, fun, acc, opts \\ []) - defp map([{key, value} | t], type, fun, acc, opts) do case fun.(type, value, opts) do - {:ok, value} -> map(t, type, fun, Map.put(acc, key, value)) + {:ok, value} -> map(t, type, fun, Map.put(acc, key, value), opts) {:error, reason} -> {:error, reason} :error -> :error end From c2d0f46877aab4cbbfd8d3ef09182fd0f2ff4d8f Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Tue, 14 Jan 2025 12:37:59 +0700 Subject: [PATCH 27/36] add changelog for 3.0.2 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b0763..726ad2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## Changelog +## v3.0.2 + +* Enhancements + * Fix warnings for Elixir 1.8.x + * Add experimental hook `Construct.Hooks.Fasten` that works faster that standart make + ## v3.0.1 * Enhancements From 0d28fdcfcc53f173ee25a7f0a3c1c4bb6844d39a Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Tue, 14 Jan 2025 12:38:03 +0700 Subject: [PATCH 28/36] release 3.0.2 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 910e9c0..a82f25f 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Construct.Mixfile do def project do [ app: :construct, - version: "3.1.0", + version: "3.0.2", elixir: "~> 1.14", deps: deps(), elixirc_paths: elixirc_paths(Mix.env), From 1b9021528f5f62cdb5ee6d0a824f8539a0d984a7 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Wed, 15 Jan 2025 15:58:46 +0700 Subject: [PATCH 29/36] add able to override cast/2 --- lib/construct.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/construct.ex b/lib/construct.ex index e969ad4..57591bd 100644 --- a/lib/construct.ex +++ b/lib/construct.ex @@ -80,7 +80,7 @@ defmodule Construct do make(params, opts) end - defoverridable make: 2 + defoverridable make: 2, cast: 2 end end From f26e50abca725bd5225e6719cc5a0cfff9a9233e Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Wed, 15 Jan 2025 15:59:19 +0700 Subject: [PATCH 30/36] remove useless randomly failing test --- test/integration/make_test.exs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/integration/make_test.exs b/test/integration/make_test.exs index fb421f4..17ec8ec 100644 --- a/test/integration/make_test.exs +++ b/test/integration/make_test.exs @@ -527,10 +527,6 @@ defmodule Construct.Integration.MakeTest do assert %{a: "a", b: %{c: 1}} = make!(module, %{a: "a", b: %{c: "1"}}) - assert_raise(Construct.MakeError, ~s(%{a: {:missing, nil}, b: {:missing, nil}}), fn -> - make!(module, %{}) - end) - assert_raise(Construct.MakeError, ~s(%{b: {:missing, nil}}), fn -> make!(module, %{a: "a"}) end) From f3c70cf63f75e26f88437a688db33de6f1d5f0e8 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Wed, 15 Jan 2025 15:59:29 +0700 Subject: [PATCH 31/36] release 3.0.3 --- CHANGELOG.md | 5 +++++ mix.exs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 726ad2b..fcfd873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Changelog +## v3.0.3 + +* Enhancements + * Able to override cast/2 + ## v3.0.2 * Enhancements diff --git a/mix.exs b/mix.exs index a82f25f..2cbbd02 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Construct.Mixfile do def project do [ app: :construct, - version: "3.0.2", + version: "3.0.3", elixir: "~> 1.14", deps: deps(), elixirc_paths: elixirc_paths(Mix.env), From 0d10758a8f7fc4441fd39c0359e08fc02cb7de5a Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Sat, 1 Feb 2025 17:14:19 +0700 Subject: [PATCH 32/36] fix fasten hook to properly return errors --- lib/construct/hooks/fasten.ex | 12 +++++-- test/integration/hooks/fasten_test.exs | 48 ++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 test/integration/hooks/fasten_test.exs diff --git a/lib/construct/hooks/fasten.ex b/lib/construct/hooks/fasten.ex index a7941e2..b3d6876 100644 --- a/lib/construct/hooks/fasten.ex +++ b/lib/construct/hooks/fasten.ex @@ -63,11 +63,19 @@ defmodule Construct.Hooks.Fasten do cast_clause = quote do defp unquote(function_name)(%{unquote(to_string(name)) => term}, opts) do - Construct.Type.cast(unquote(type), term, opts) + case Construct.Type.cast(unquote(type), term, opts) do + {:ok, term} -> {:ok, term} + {:error, reason} -> {:error, %{unquote(name) => reason}} + :error -> {:error, %{unquote(name) => :invalid}} + end end defp unquote(function_name)(%{unquote(name) => term}, opts) do - Construct.Type.cast(unquote(type), term, opts) + case Construct.Type.cast(unquote(type), term, opts) do + {:ok, term} -> {:ok, term} + {:error, reason} -> {:error, %{unquote(name) => reason}} + :error -> {:error, %{unquote(name) => :invalid}} + end end end diff --git a/test/integration/hooks/fasten_test.exs b/test/integration/hooks/fasten_test.exs new file mode 100644 index 0000000..29adf34 --- /dev/null +++ b/test/integration/hooks/fasten_test.exs @@ -0,0 +1,48 @@ +defmodule Construct.Integration.Hooks.FastenTest do + use Construct.TestCase + + defmodule IP do + @behaviour Construct.Type + + def cast(term) when is_binary(term) do + :inet.parse_address(String.to_charlist(term)) + end + + def cast(_) do + :error + end + end + + defmodule User do + use Construct + use Construct.Hooks.Fasten + + structure do + field :huid, :string + field :ip, IP + end + end + + test "#make returns structs with anonymous functions" do + params = %{ + "huid" => "9767dcf6-9413-44dd-af9a-2af0188ae12b", + "ip" => "127.0.0.1" + } + + assert {:ok, %User{ + huid: params["huid"], + ip: {127, 0, 0, 1} + }} == User.make(params) + end + + test "#make with invalid params" do + assert {:error, %{huid: :invalid}} = + User.make(%{"huid" => 42, "ip" => 42}) + + assert {:error, %{huid: :invalid}} = + User.make(%{"huid" => 42, "ip" => "127.0.0.1.1"}) + + assert {:error, %{ip: :einval}} = + User.make(%{"huid" => "9767dcf6-9413-44dd-af9a-2af0188ae12b", "ip" => "127.0.0.1.1"}) + end +end From f9be2b682f8db9183844af8ed5683847f3023cf9 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Thu, 23 Oct 2025 15:00:22 +0300 Subject: [PATCH 33/36] make!/2 is now overridable --- lib/construct.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/construct.ex b/lib/construct.ex index 57591bd..3897177 100644 --- a/lib/construct.ex +++ b/lib/construct.ex @@ -80,7 +80,7 @@ defmodule Construct do make(params, opts) end - defoverridable make: 2, cast: 2 + defoverridable make: 2, make!: 2, cast: 2 end end From 5481b258ad5885b089460580872030d09b7c7334 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Thu, 23 Oct 2025 15:00:28 +0300 Subject: [PATCH 34/36] update ex_doc --- mix.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mix.lock b/mix.lock index 517c865..e4dd3a0 100644 --- a/mix.lock +++ b/mix.lock @@ -3,13 +3,13 @@ "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, - "ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"}, + "ex_doc": {:hex, :ex_doc, "0.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, } From 0ad40ad0c741a0ce2b6284d4ecac2c32337a2e4f Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Thu, 23 Oct 2025 15:03:08 +0300 Subject: [PATCH 35/36] release 3.0.4 --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 2cbbd02..61b1d4b 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Construct.Mixfile do def project do [ app: :construct, - version: "3.0.3", + version: "3.0.4", elixir: "~> 1.14", deps: deps(), elixirc_paths: elixirc_paths(Mix.env), From f793278748c9083bbd46aa71de81864d5608ace1 Mon Sep 17 00:00:00 2001 From: Yuri Artemev Date: Thu, 23 Oct 2025 15:05:05 +0300 Subject: [PATCH 36/36] use Code.eval_quoted for fasten hook --- lib/construct/hooks/fasten.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/construct/hooks/fasten.ex b/lib/construct/hooks/fasten.ex index b3d6876..f3a3eb2 100644 --- a/lib/construct/hooks/fasten.ex +++ b/lib/construct/hooks/fasten.ex @@ -2,7 +2,7 @@ defmodule Construct.Hooks.Fasten do defmacro __using__(_opts \\ []) do quote do structure_compile_hook :post do - Module.eval_quoted(__MODULE__, Construct.Hooks.Fasten.__compile__(__MODULE__, Enum.reverse(@fields))) + Code.eval_quoted(Construct.Hooks.Fasten.__compile__(__MODULE__, Enum.reverse(@fields)), [], __ENV__) defoverridable make: 2 end