diff --git a/.formatter.exs b/.formatter.exs index 5c56972..048ad18 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -3,7 +3,7 @@ locals_without_parens = [field: 2, field: 3, parameter: 1, plugin: 1, plugin: 2] [ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], import_deps: [:ecto], - locals_without_parens: locals_without_parens, + locals_without_parens: [{:assert_type, 2} | locals_without_parens], export: [ locals_without_parens: locals_without_parens ] diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index d91e5f6..a83cef2 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -72,5 +72,10 @@ jobs: run: mix dialyzer --format github if: ${{ matrix.lint }} + - name: Run tests + run: mix test --cover + if: ${{ matrix.lint }} + - name: Run tests run: mix test + if: ${{ !matrix.lint }} diff --git a/mix.exs b/mix.exs index 46d3b4d..fb90ff9 100644 --- a/mix.exs +++ b/mix.exs @@ -48,6 +48,14 @@ defmodule TypedStructor.MixProject do links: %{ "GitHub" => @source_url } + ], + test_coverage: [ + summary: [threshold: 100], + ignore_modules: [ + TypedStructor.Definition, + TypedStructor.GuideCase, + TypedStructor.TestCase + ] ] ] end diff --git a/test/config_test.exs b/test/config_test.exs index 7a04748..e877976 100644 --- a/test/config_test.exs +++ b/test/config_test.exs @@ -1,6 +1,8 @@ defmodule ConfigTest do + @compile {:no_warn_undefined, ConfigTest.Struct} + # disable async for this test for changing the application env - use TypedStructor.TypeCase, async: false + use TypedStructor.TestCase, async: false defmodule Plugin do use TypedStructor.Plugin @@ -24,25 +26,29 @@ defmodule ConfigTest do end end - test "registers plugins from the config" do + @tag :tmp_dir + test "registers plugins from the config", ctx do set_plugins_config([Plugin, {PluginWithOpts, [foo: :bar]}]) - deftmpmodule do - use TypedStructor + plugin_calls = + with_tmpmodule Struct, ctx do + use TypedStructor - Module.register_attribute(__MODULE__, :plugin_calls, accumulate: true) + Module.register_attribute(__MODULE__, :plugin_calls, accumulate: true) - typed_structor do - field :name, String.t() - end + typed_structor do + field :name, String.t() + end - def plugin_calls, do: @plugin_calls - end + def plugin_calls, do: @plugin_calls + after + Struct.plugin_calls() + end assert [ {PluginWithOpts, [foo: :bar]}, {Plugin, []} - ] === TestModule.plugin_calls() + ] === plugin_calls end test "raises if the plugin is not a module" do @@ -51,7 +57,7 @@ defmodule ConfigTest do assert_raise ArgumentError, ~r/Expected a plugin module or a tuple with a plugin module and its keyword options/, fn -> - test_module do + defmodule Struct do use TypedStructor typed_structor do @@ -67,7 +73,7 @@ defmodule ConfigTest do assert_raise ArgumentError, ~r/Expected a plugin module or a tuple with a plugin module and its keyword options/, fn -> - test_module do + defmodule Struct do use TypedStructor typed_structor do diff --git a/test/doc_test.exs b/test/doc_test.exs new file mode 100644 index 0000000..4fc7f5c --- /dev/null +++ b/test/doc_test.exs @@ -0,0 +1,64 @@ +defmodule DocTest do + use TypedStructor.TestCase, async: true + + @tag :tmp_dir + test "typedoc", ctx do + generated_doc = + with_tmpmodule User, ctx do + use TypedStructor + + @typedoc "A user struct" + typed_structor do + field :name, String.t() + field :age, integer() + end + after + fetch_doc!(User, {:type, :t, 0}) + end + + assert "A user struct" === generated_doc + end + + @tag :tmp_dir + test "typedoc inside block", ctx do + generated_doc = + with_tmpmodule User, ctx do + use TypedStructor + + typed_structor do + @typedoc "A user struct" + field :name, String.t() + field :age, integer() + end + after + fetch_doc!(User, {:type, :t, 0}) + end + + assert "A user struct" === generated_doc + end + + @tag :tmp_dir + test "moduledoc and typedoc inside submodule's block", ctx do + generated_docs = + with_tmpmodule MyModule, ctx do + use TypedStructor + + typed_structor module: User do + @moduledoc "A user module" + @typedoc "A user struct" + field :name, String.t() + field :age, integer() + end + after + { + fetch_doc!(MyModule.User, :moduledoc), + fetch_doc!(MyModule.User, {:type, :t, 0}) + } + |> tap(fn _ -> + cleanup_modules([MyModule.User], ctx.tmp_dir) + end) + end + + assert {"A user module", "A user struct"} === generated_docs + end +end diff --git a/test/support/guide_case.ex b/test/support/guide_case.ex index d9f7e1e..e308589 100644 --- a/test/support/guide_case.ex +++ b/test/support/guide_case.ex @@ -23,7 +23,10 @@ defmodule TypedStructor.GuideCase do end def types(bytecode) when is_binary(bytecode) do - TypedStructor.TypeCase.types(bytecode) <> "\n" + bytecode + |> TypedStructor.TestCase.fetch_types!() + |> TypedStructor.TestCase.format_types() + |> Kernel.<>("\n") end defp extract_code(file) do diff --git a/test/support/test_case.ex b/test/support/test_case.ex new file mode 100644 index 0000000..4d5347c --- /dev/null +++ b/test/support/test_case.ex @@ -0,0 +1,195 @@ +defmodule TypedStructor.TestCase do + @moduledoc false + use ExUnit.CaseTemplate + + setup ctx do + if Map.has_key?(ctx, :tmp_dir) do + true = Code.append_path(ctx.tmp_dir) + on_exit(fn -> Code.delete_path(ctx.tmp_dir) end) + end + + :ok + end + + using do + quote do + import unquote(__MODULE__) + end + end + + @doc """ + Defines a temporary module with the given `module_name` and executes the code + in the `after` block. The module is removed after the block is executed. + And the `after` block's return value is returned. + + Note that the `module_name` is expanded to the caller's module. + """ + defmacro with_tmpmodule(module_name, ctx, options) when is_list(options) do + module_name = + module_name + |> Macro.expand(__CALLER__) + |> then(&Module.concat(__CALLER__.module, &1)) + + code = Keyword.fetch(options, :do) + + content = + """ + defmodule #{Atom.to_string(module_name)} do + #{Macro.to_string(code)} + end + """ + + fun = + quote do + fn -> + alias unquote(module_name) + unquote(Keyword.get(options, :after)) + end + end + + quote do + unquote(__MODULE__).__with_file__( + unquote(ctx), + {unquote(module_name), unquote(content)}, + unquote(fun) + ) + end + end + + @doc false + def __with_file__(%{tmp_dir: dir}, {module_name, content}, fun) when is_function(fun, 0) do + path = Path.join([dir, Atom.to_string(module_name)]) + + try do + File.write!(path, content) + compile_file!(path, dir) + + fun.() + after + File.rm!(path) + cleanup_modules([module_name], dir) + end + end + + @doc """ + Defines a temporary module with the given `module_name`, + returns the compiled modules. + + You should clean up the modules by calling `cleanup_modules/2` + after you are done. + + Note that the `module_name` is expanded to the caller's module + like `with_tmpmodule/3`. + """ + defmacro deftmpmodule(module_name, ctx, do: block) do + module_name = + module_name + |> Macro.expand(__CALLER__) + |> then(&Module.concat(__CALLER__.module, &1)) + + content = + """ + defmodule #{Atom.to_string(module_name)} do + #{Macro.to_string(block)} + end + """ + + quote do + alias unquote(module_name) + + unquote(__MODULE__).__compile_tmpmodule__( + unquote(ctx), + {unquote(module_name), unquote(content)} + ) + end + end + + @doc false + def __compile_tmpmodule__(%{tmp_dir: dir}, {module_name, content}) do + path = Path.join([dir, Atom.to_string(module_name)]) + + File.write!(path, content) + compile_file!(path, dir) + end + + defp compile_file!(path, dir) do + Code.compiler_options(docs: true, debug_info: true) + {:ok, modules, []} = Kernel.ParallelCompiler.compile_to_path(List.wrap(path), dir) + + modules + end + + @doc """ + Cleans up the modules by removing the beam files and purging the code. + """ + @spec cleanup_modules([module()], dir :: Path.t()) :: term() + def cleanup_modules(mods, dir) do + Enum.each(mods, fn mod -> + File.rm(Path.join([dir, "#{mod}.beam"])) + :code.purge(mod) + true = :code.delete(mod) + end) + end + + @doc """ + Fetches the types for the given module. + """ + @spec fetch_types!(module() | binary) :: [tuple()] + def fetch_types!(module) when is_atom(module) or is_binary(module) do + module + |> Code.Typespec.fetch_types() + |> case do + :error -> refute "Failed to fetch types for module #{module}" + {:ok, types} -> types + end + end + + @doc """ + Fetches the doc for the given module or its functions and types. + """ + def fetch_doc!(module, :moduledoc) when is_atom(module) do + case Code.fetch_docs(module) do + {:docs_v1, _, :elixir, _, %{"en" => doc}, _, _} -> doc + _ -> refute "Failed to fetch moduledoc for #{module}" + end + end + + def fetch_doc!(module, {type, name, arity}) when is_atom(module) do + with( + {:docs_v1, _, :elixir, _, _, _, docs} <- Code.fetch_docs(module), + {_, _, _, %{"en" => doc}, _} <- List.keyfind(docs, {type, name, arity}, 0) + ) do + doc + else + _other -> refute "Failed to fetch doc for #{inspect({type, name, arity})} at #{module}" + end + end + + @doc """ + Asserts that the expected types are equal to the actual types by comparing + their formatted strings. + """ + @spec assert_type(expected :: [tuple()], actual :: [tuple()]) :: term() + def assert_type(expected, actual) do + expected_types = format_types(expected) + + if String.length(String.trim(expected_types)) === 0 do + refute "Expected types are empty: #{inspect(expected)}" + end + + assert expected_types == format_types(actual) + end + + @spec format_types([tuple()]) :: String.t() + def format_types(types) do + types + |> Enum.sort_by(fn {_, {name, _, args}} -> {name, length(args)} end) + |> Enum.map_join( + "\n", + fn {kind, type} -> + ast = Code.Typespec.type_to_quoted(type) + "@#{kind} #{Macro.to_string(ast)}" + end + ) + end +end diff --git a/test/support/type_case.ex b/test/support/type_case.ex deleted file mode 100644 index a5141ed..0000000 --- a/test/support/type_case.ex +++ /dev/null @@ -1,91 +0,0 @@ -defmodule TypedStructor.TypeCase do - @moduledoc false - - use ExUnit.CaseTemplate - - setup do - Code.compiler_options(debug_info: true) - - :ok - end - - using do - quote do - import unquote(__MODULE__) - end - end - - defmacro deftmpmodule(do: block) do - quote do - {:module, module_name, bytecode, submodule} = - defmodule TestModule do - # credo:disable-for-previous-line Credo.Check.Readability.ModuleDoc - unquote(block) - end - - case submodule do - {:module, submodule_name, bytecode, _} -> - on_exit(fn -> - remove_module(module_name) - remove_module(submodule_name) - end) - - bytecode - - _other -> - on_exit(fn -> - remove_module(module_name) - end) - - bytecode - end - end - end - - defmacro test_module(do: block) do - quote do - {:module, module_name, bytecode, submodule} = - returning = - defmodule TestModule do - # credo:disable-for-previous-line Credo.Check.Readability.ModuleDoc - unquote(block) - end - - case submodule do - {:module, submodule_name, bytecode, _} -> - remove_module(module_name) - remove_module(submodule_name) - - bytecode - - _other -> - remove_module(module_name) - - bytecode - end - end - end - - def remove_module(module) do - :code.delete(module) - :code.purge(module) - end - - def types(module) when is_binary(module) or is_atom(module) do - module - |> Code.Typespec.fetch_types() - |> elem(1) - |> Enum.sort_by(fn {_, {name, _, args}} -> {name, length(args)} end) - |> Enum.map_join( - "\n", - fn {kind, type} -> - ast = Code.Typespec.type_to_quoted(type) - format_typespec(ast, kind) - end - ) - end - - defp format_typespec(ast, kind) do - "@#{kind} #{Macro.to_string(ast)}" - end -end diff --git a/test/type_and_enforce_keys_test.exs b/test/type_and_enforce_keys_test.exs index 47286dd..c541fda 100644 --- a/test/type_and_enforce_keys_test.exs +++ b/test/type_and_enforce_keys_test.exs @@ -1,139 +1,128 @@ defmodule TypeAndEnforceKeysTest do - use TypedStructor.TypeCase, async: true + use TypedStructor.TestCase, async: true - test "default is set and enforce is true" do - expected_bytecode = - test_module do - @type t() :: %TestModule{ + @tag :tmp_dir + test "default is set and enforce is true", ctx do + expected_types = + with_tmpmodule Struct, ctx do + @type t() :: %__MODULE__{ fixed: boolean() } defstruct [:fixed] + after + fetch_types!(Struct) end - expected_types = types(expected_bytecode) - - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule Struct, ctx do use TypedStructor typed_structor do field :fixed, boolean(), default: true, enforce: true end - end + after + assert %{__struct__: Struct, fixed: true} === build_struct(quote(do: %Struct{})) - assert expected_types === types(bytecode) + fetch_types!(Struct) + end - assert match?( - %{ - __struct__: TestModule, - fixed: true - }, - build_struct(quote(do: %TestModule{})) - ) + assert_type expected_types, generated_types end - test "default is set and enforce is false" do - expected_bytecode = - test_module do - @type t() :: %TestModule{ + @tag :tmp_dir + test "default is set and enforce is false", ctx do + expected_types = + with_tmpmodule Struct, ctx do + @type t() :: %__MODULE__{ fixed: boolean() } defstruct [:fixed] + after + fetch_types!(Struct) end - expected_types = types(expected_bytecode) - - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule Struct, ctx do use TypedStructor typed_structor do field :fixed, boolean(), default: true end - end + after + assert %{__struct__: Struct, fixed: true} === build_struct(quote(do: %Struct{})) - assert expected_types === types(bytecode) + fetch_types!(Struct) + end - assert match?( - %{ - __struct__: TestModule, - fixed: true - }, - build_struct(quote(do: %TestModule{})) - ) + assert_type expected_types, generated_types end - test "default is unset and enforce is true" do - expected_bytecode = - test_module do - @type t() :: %TestModule{ + @tag :tmp_dir + test "default is unset and enforce is true", ctx do + expected_types = + with_tmpmodule Struct, ctx do + @type t() :: %__MODULE__{ fixed: boolean() } defstruct [:fixed] + after + fetch_types!(Struct) end - expected_types = types(expected_bytecode) - - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule Struct, ctx do use TypedStructor typed_structor do field :fixed, boolean(), enforce: true end - end + after + assert_raise_on_enforce_error([:fixed], quote(do: %Struct{})) - assert expected_types === types(bytecode) + assert %{__struct__: Struct, fixed: true} === + build_struct(quote(do: %Struct{fixed: true})) - assert_raise_on_enforce_error([:fixed], quote(do: %TestModule{})) + fetch_types!(Struct) + end - assert match?( - %{ - __struct__: TestModule, - fixed: true - }, - build_struct(quote(do: %TestModule{fixed: true})) - ) + assert_type expected_types, generated_types end - test "default is unset and enforce is false" do - expected_bytecode = - test_module do - @type t() :: %TestModule{ + @tag :tmp_dir + test "default is unset and enforce is false", ctx do + expected_types = + with_tmpmodule Struct, ctx do + @type t() :: %__MODULE__{ fixed: boolean() | nil } defstruct [:fixed] + after + fetch_types!(Struct) end - expected_types = types(expected_bytecode) - - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule Struct, ctx do use TypedStructor typed_structor do field :fixed, boolean() end - end + after + assert %{__struct__: Struct, fixed: nil} === build_struct(quote(do: %Struct{})) - assert expected_types === types(bytecode) + fetch_types!(Struct) + end - assert match?( - %{ - __struct__: TestModule, - fixed: nil - }, - build_struct(quote(do: %TestModule{})) - ) + assert_type expected_types, generated_types end defp assert_raise_on_enforce_error(keys, quoted) do assert_raise ArgumentError, - "the following keys must also be given when building struct #{inspect(__MODULE__.TestModule)}: #{inspect(keys)}", + "the following keys must also be given when building struct #{inspect(__MODULE__.Struct)}: #{inspect(keys)}", fn -> Code.eval_quoted(quoted) end diff --git a/test/typed_structor/plugin_test.exs b/test/typed_structor/plugin_test.exs index 62ad9c6..ce48203 100644 --- a/test/typed_structor/plugin_test.exs +++ b/test/typed_structor/plugin_test.exs @@ -1,5 +1,5 @@ defmodule TypedStructor.PluginTest do - use TypedStructor.TypeCase, async: true + use TypedStructor.TestCase, async: true describe "callbacks order" do for plugin <- [Plugin1, Plugin2] do @@ -57,54 +57,59 @@ defmodule TypedStructor.PluginTest do end describe "before_definition/2" do - defmodule ManipulatePlugin do - use TypedStructor.Plugin - - @impl TypedStructor.Plugin - defmacro before_definition(definition, _plugin_opts) do - quote do - Map.update!( - unquote(definition), - :fields, - fn fields -> - Enum.map(fields, fn field -> - {name, field} = Keyword.pop!(field, :name) - {type, field} = Keyword.pop!(field, :type) - name = name |> Atom.to_string() |> String.upcase() |> String.to_atom() - type = quote do: unquote(type) | atom() - - [{:name, name}, {:type, type} | field] - end) - end - ) + @tag :tmp_dir + test "manipulates definition", ctx do + deftmpmodule ManipulatePlugin, ctx do + use TypedStructor.Plugin + + @impl TypedStructor.Plugin + defmacro before_definition(definition, _plugin_opts) do + quote do + Map.update!( + unquote(definition), + :fields, + fn fields -> + Enum.map(fields, fn field -> + {name, field} = Keyword.pop!(field, :name) + {type, field} = Keyword.pop!(field, :type) + name = name |> Atom.to_string() |> String.upcase() |> String.to_atom() + type = quote do: unquote(type) | atom() + + [{:name, name}, {:type, type} | field] + end) + end + ) + end end end - end - test "manipulates definition" do - expected_bytecode = - test_module do - @type t() :: %TestModule{ + expected_types = + with_tmpmodule MyStruct, ctx do + @type t() :: %__MODULE__{ NAME: (String.t() | atom()) | nil } defstruct [:NAME] + after + fetch_types!(MyStruct) end - expected_types = types(expected_bytecode) - - bytecode = - test_module do + types = + with_tmpmodule MyStruct, ctx do use TypedStructor typed_structor do - plugin ManipulatePlugin + plugin unquote(__MODULE__).ManipulatePlugin field :name, String.t() end + after + fetch_types!(MyStruct) end - assert expected_types === types(bytecode) + assert_type expected_types, types + after + cleanup_modules([__MODULE__.ManipulatePlugin], ctx.tmp_dir) end end end diff --git a/test/typed_structor_test.exs b/test/typed_structor_test.exs index 80fb2e3..1751942 100644 --- a/test/typed_structor_test.exs +++ b/test/typed_structor_test.exs @@ -1,155 +1,161 @@ defmodule TypedStructorTest do - use TypedStructor.TypeCase, async: true + @compile {:no_warn_undefined, __MODULE__.Struct} - test "generates the struct and the type" do - expected_bytecode = - test_module do - @type t() :: %TestModule{ + use TypedStructor.TestCase, async: true + + @tag :tmp_dir + test "generates the struct and the type", ctx do + expected_types = + with_tmpmodule Struct, ctx do + @type t() :: %__MODULE__{ age: integer() | nil, name: String.t() | nil } defstruct [:age, :name] + after + fetch_types!(Struct) end - expected_types = types(expected_bytecode) - - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule Struct, ctx do use TypedStructor typed_structor do field :name, String.t() field :age, integer() end - end + after + assert %{__struct__: Struct, name: nil, age: nil} === struct(Struct) - assert match?( - %{ - __struct__: TestModule, - name: nil, - age: nil - }, - struct(TestModule) - ) + fetch_types!(Struct) + end - assert expected_types === types(bytecode) + assert_type expected_types, generated_types end describe "module option" do - test "generates the struct and the type" do - expected_bytecode = - test_module do + @tag :tmp_dir + test "generates the struct and the type", ctx do + expected_types = + with_tmpmodule MyModule, ctx do defmodule Struct do - @type t() :: %TestModule.Struct{ + @type t() :: %__MODULE__{ age: integer() | nil, name: String.t() | nil } defstruct [:age, :name] end + after + fetch_types!(MyModule.Struct) end - expected_types = types(expected_bytecode) + cleanup_modules([__MODULE__.MyModule.Struct], ctx.tmp_dir) - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule MyModule, ctx do use TypedStructor typed_structor module: Struct do field :name, String.t() field :age, integer() end + after + assert %{__struct__: MyModule.Struct, name: nil, age: nil} === + struct(MyModule.Struct) + + fetch_types!(MyModule.Struct) end - assert match?( - %{ - __struct__: TestModule.Struct, - name: nil, - age: nil - }, - struct(TestModule.Struct) - ) + cleanup_modules([__MODULE__.MyModule.Struct], ctx.tmp_dir) - assert expected_types === types(bytecode) + assert_type expected_types, generated_types end end describe "enforce option" do - test "set enforce on fields" do - expected_bytecode = - test_module do - @type t() :: %TestModule{ + @tag :tmp_dir + test "set enforce on fields", ctx do + expected_types = + with_tmpmodule Struct, ctx do + @type t() :: %__MODULE__{ name: String.t(), age: integer() | nil } defstruct [:age, :name] + after + fetch_types!(Struct) end - expected_types = types(expected_bytecode) - - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule Struct, ctx do use TypedStructor typed_structor do field :name, String.t(), enforce: true field :age, integer() end - end + after + assert_raise_on_enforce_error(Struct, [:name], quote(do: %Struct{})) - assert_raise_on_enforce_error(TestModule, [:name], quote(do: %TestModule{})) + fetch_types!(Struct) + end - assert expected_types === types(bytecode) + assert_type expected_types, generated_types end - test "set enforce on typed_structor" do - expected_bytecode = - test_module do - @type t() :: %TestModule{ + @tag :tmp_dir + test "set enforce on typed_structor", ctx do + expected_types = + with_tmpmodule Struct, ctx do + @type t() :: %__MODULE__{ name: String.t(), age: integer() } defstruct [:age, :name] + after + fetch_types!(Struct) end - expected_types = types(expected_bytecode) - - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule Struct, ctx do use TypedStructor typed_structor enforce: true do field :name, String.t() field :age, integer() end + after + assert_raise_on_enforce_error( + Struct, + [:name, :age], + quote(do: %Struct{}) + ) + + fetch_types!(Struct) end - assert_raise_on_enforce_error( - TestModule, - [:name, :age], - quote(do: %TestModule{}) - ) - - assert expected_types === types(bytecode) + assert_type expected_types, generated_types end - test "overwrites the enforce option on fields" do - expected_bytecode = - test_module do - @type t() :: %TestModule{ + @tag :tmp_dir + test "overwrites the enforce option on fields", ctx do + expected_types = + with_tmpmodule Struct, ctx do + @type t() :: %__MODULE__{ name: String.t(), age: integer() | nil } defstruct [:age, :name] + after + fetch_types!(Struct) end - expected_types = types(expected_bytecode) - - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule Struct, ctx do use TypedStructor typed_structor enforce: true do @@ -158,65 +164,67 @@ defmodule TypedStructorTest do end def enforce_keys, do: @enforce_keys - end + after + assert_raise_on_enforce_error(Struct, [:name], quote(do: %Struct{})) - assert_raise_on_enforce_error( - TestModule, - [:name], - quote(do: %TestModule{}) - ) + assert [:name] === Struct.enforce_keys() - assert [:name] === TestModule.enforce_keys() + fetch_types!(Struct) + end - assert expected_types === types(bytecode) + assert expected_types, generated_types end end describe "type_kind option" do - test "generates opaque type" do - expected_bytecode = - test_module do - @opaque t() :: %TestModule{ + @tag :tmp_dir + test "generates opaque type", ctx do + expected_types = + with_tmpmodule Struct, ctx do + @opaque t() :: %__MODULE__{ name: String.t() | nil, age: integer() | nil } defstruct [:age, :name] + after + fetch_types!(Struct) end - expected_types = types(expected_bytecode) - - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule Struct, ctx do use TypedStructor typed_structor type_kind: :opaque do field :name, String.t() field :age, integer() end + after + fetch_types!(Struct) end - assert expected_types === types(bytecode) + assert_type expected_types, generated_types end - test "generates typep type" do - expected_bytecode = - test_module do + @tag :tmp_dir + test "generates typep type", ctx do + expected_types = + with_tmpmodule Struct, ctx do # suppress unused warning @type external_t() :: t() - @typep t() :: %TestModule{ + @typep t() :: %__MODULE__{ name: String.t() | nil, age: integer() | nil } defstruct [:age, :name] + after + fetch_types!(Struct) end - expected_types = types(expected_bytecode) - - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule Struct, ctx do use TypedStructor # suppress unused warning @@ -226,56 +234,62 @@ defmodule TypedStructorTest do field :name, String.t() field :age, integer() end + after + fetch_types!(Struct) end - assert expected_types === types(bytecode) + assert_type expected_types, generated_types end end describe "type_name option" do - test "generates custom type_name type" do - expected_bytecode = - test_module do - @type test_type() :: %TestModule{ + @tag :tmp_dir + test "generates custom type_name type", ctx do + expected_types = + with_tmpmodule Struct, ctx do + @type test_type() :: %__MODULE__{ name: String.t() | nil, age: integer() | nil } defstruct [:age, :name] + after + fetch_types!(Struct) end - expected_types = types(expected_bytecode) - - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule Struct, ctx do use TypedStructor typed_structor type_name: :test_type do field :name, String.t() field :age, integer() end + after + fetch_types!(Struct) end - assert expected_types === types(bytecode) + assert_type expected_types, generated_types end end describe "default option on the field" do - test "generates struct with default values" do - expected_bytecode = - test_module do - @type t() :: %TestModule{ + @tag :tmp_dir + test "generates struct with default values", ctx do + expected_types = + with_tmpmodule Struct, ctx do + @type t() :: %__MODULE__{ name: String.t(), age: integer() | nil } defstruct [:age, :name] + after + fetch_types!(Struct) end - expected_types = types(expected_bytecode) - - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule Struct, ctx do use TypedStructor typed_structor do @@ -284,39 +298,35 @@ defmodule TypedStructorTest do end def enforce_keys, do: @enforce_keys - end + after + assert %{__struct__: Struct, name: "Phil", age: nil} === struct(Struct) - assert match?( - %{ - __struct__: TestModule, - name: "Phil", - age: nil - }, - struct(TestModule) - ) + assert [] === Struct.enforce_keys() - assert [] === TestModule.enforce_keys() + fetch_types!(Struct) + end - assert expected_types === types(bytecode) + assert_type expected_types, generated_types end end describe "parameter" do - test "generates parameterized type" do - expected_bytecode = - test_module do - @type t(age) :: %TestModule{ + @tag :tmp_dir + test "generates parameterized type", ctx do + expected_types = + with_tmpmodule Struct, ctx do + @type t(age) :: %__MODULE__{ age: age | nil, name: String.t() | nil } defstruct [:age, :name] + after + fetch_types!(Struct) end - expected_types = types(expected_bytecode) - - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule Struct, ctx do use TypedStructor typed_structor do @@ -325,35 +335,31 @@ defmodule TypedStructorTest do field :name, String.t() field :age, age end - end + after + assert %{__struct__: Struct, name: nil, age: nil} === struct(Struct) - assert match?( - %{ - __struct__: TestModule, - name: nil, - age: nil - }, - struct(TestModule) - ) + fetch_types!(Struct) + end - assert expected_types === types(bytecode) + assert_type expected_types, generated_types end - test "generates ordered parameters for the type" do - expected_bytecode = - test_module do - @type t(age, name) :: %TestModule{ + @tag :tmp_dir + test "generates ordered parameters for the type", ctx do + expected_types = + with_tmpmodule Struct, ctx do + @type t(age, name) :: %__MODULE__{ age: age | nil, name: name | nil } defstruct [:name, :age] + after + fetch_types!(Struct) end - expected_types = types(expected_bytecode) - - bytecode = - deftmpmodule do + generated_types = + with_tmpmodule Struct, ctx do use TypedStructor typed_structor do @@ -363,24 +369,32 @@ defmodule TypedStructorTest do field :name, name field :age, age end + after + assert %{__struct__: Struct, name: nil, age: nil} === struct(Struct) + + fetch_types!(Struct) end - assert match?( - %{ - __struct__: TestModule, - name: nil, - age: nil - }, - struct(TestModule) - ) + assert_type expected_types, generated_types + end + + test "raises an error when the parameter is not a atom" do + assert_raise ArgumentError, ~r[expected an atom, got: "age"], fn -> + defmodule Struct do + use TypedStructor - assert expected_types === types(bytecode) + typed_structor do + parameter "age" + end + end + end end end describe "define_struct option" do - test "implements Access" do - deftmpmodule do + @tag :tmp_dir + test "implements Access", ctx do + deftmpmodule Struct, ctx do use TypedStructor typed_structor define_struct: false do @@ -393,20 +407,16 @@ defmodule TypedStructorTest do defstruct name: "Phil", age: 20 end - assert match?( - %{ - __struct__: TestModule, - name: "Phil", - age: 20 - }, - struct(TestModule) - ) + assert %{__struct__: Struct, name: "Phil", age: 20} === struct(Struct) + after + cleanup_modules([__MODULE__.Struct], ctx.tmp_dir) end end describe "works with Ecto.Schema" do - test "works" do - deftmpmodule do + @tag :tmp_dir + test "works", ctx do + deftmpmodule Struct, ctx do use TypedStructor typed_structor define_struct: false do @@ -424,16 +434,11 @@ defmodule TypedStructorTest do end end - assert [:id, :name, :age] === TestModule.__schema__(:fields) + assert [:id, :name, :age] === Struct.__schema__(:fields) - assert match?( - %{ - __struct__: TestModule, - name: nil, - age: 20 - }, - struct(TestModule) - ) + assert match?(%{__struct__: Struct, id: nil, name: nil, age: 20}, struct(Struct)) + after + cleanup_modules([__MODULE__.Struct], ctx.tmp_dir) end end