From d8f9d96a6954ba0f0ca3796997129f546b6489f2 Mon Sep 17 00:00:00 2001 From: Phil Chen <06fahchen@gmail.com> Date: Sat, 6 Jul 2024 11:41:52 +0800 Subject: [PATCH 1/5] test: refactor test_case (#9) Fetching types and docs from modules by compiling them into a temporary file. --- .formatter.exs | 2 +- .github/workflows/elixir.yml | 5 + mix.exs | 8 + test/config_test.exs | 32 ++- test/doc_test.exs | 64 +++++ test/support/guide_case.ex | 5 +- test/support/test_case.ex | 195 +++++++++++++++ test/support/type_case.ex | 91 ------- test/type_and_enforce_keys_test.exs | 131 +++++----- test/typed_structor/plugin_test.exs | 69 +++--- test/typed_structor_test.exs | 363 ++++++++++++++-------------- 11 files changed, 577 insertions(+), 388 deletions(-) create mode 100644 test/doc_test.exs create mode 100644 test/support/test_case.ex delete mode 100644 test/support/type_case.ex 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 From 5906967a23071461d6d1f22cbb524fe56f184aef Mon Sep 17 00:00:00 2001 From: Phil Chen <06fahchen@gmail.com> Date: Sat, 6 Jul 2024 11:43:37 +0800 Subject: [PATCH 2/5] doc: update links --- README.md | 4 ++-- lib/typed_structor/plugin.ex | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ccaa893..3ab24f2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # TypedStructor [![Build Status](https://github.com/elixir-typed-structor/typed_structor/actions/workflows/elixir.yml/badge.svg)](https://github.com/elixir-typed-structor/typed_structor/actions/workflows/elixir.yml) -[![Hex.pm](https://img.shields.io/hexpm/v/typed_structor.svg)](https://hex.pm/packages/typed_structor) -[![Documentation](https://img.shields.io/badge/documentation-gray)](https://hexdocs.pm/typed_structor/) +[![Hex.pm](https://img.shields.io/hexpm/v/typed_structor?style=flat)](https://hex.pm/packages/typed_structor) +[![Document](https://img.shields.io/badge/hex-doc-blue?style=flat)](https://hexdocs.pm/typed_structor) diff --git a/lib/typed_structor/plugin.ex b/lib/typed_structor/plugin.ex index 84d9c20..6b9e038 100644 --- a/lib/typed_structor/plugin.ex +++ b/lib/typed_structor/plugin.ex @@ -13,6 +13,12 @@ defmodule TypedStructor.Plugin do * `c:after_definition/2`: This macro callback is called right after defining the struct. Note that plugins will run in the **reverse** order they are registered. + > #### Plugins guides {: .tip} + > + > Here are some [Plugin Guides](guides/plugins/introduction.md) + > for creating your own plugins. Please check them out + > and feel free to copy-paste the code. + ### Example Let's define a plugin that defines `Ecto.Schema` while defining a typed struct. From df5b17e09cb569cb1c93c470db0e9eb322e429b8 Mon Sep 17 00:00:00 2001 From: Phil Chen <06fahchen@gmail.com> Date: Sun, 7 Jul 2024 19:37:52 +0800 Subject: [PATCH 3/5] refactor!: store parameters as keywords instead of atoms to retain more information (#10) --- .formatter.exs | 2 +- guides/plugins/reflection.md | 2 +- lib/typed_structor.ex | 25 ++++++++++++++++----- lib/typed_structor/definition.ex | 2 +- test/config_test.exs | 2 +- test/support/test_case.ex | 38 +++++++++++++++++++------------- test/typed_structor_test.exs | 20 +++++++++-------- 7 files changed, 57 insertions(+), 34 deletions(-) diff --git a/.formatter.exs b/.formatter.exs index 048ad18..1360534 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,4 @@ -locals_without_parens = [field: 2, field: 3, parameter: 1, plugin: 1, plugin: 2] +locals_without_parens = [field: 2, field: 3, parameter: 1, parameter: 2, plugin: 1, plugin: 2] [ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], diff --git a/guides/plugins/reflection.md b/guides/plugins/reflection.md index a51d30a..6f72f90 100644 --- a/guides/plugins/reflection.md +++ b/guides/plugins/reflection.md @@ -21,7 +21,7 @@ defmodule Guides.Plugins.Reflection do |> Enum.to_list() def __typed_structor__(:fields), do: unquote(fields) - def __typed_structor__(:parameters), do: unquote(definition.parameters) + def __typed_structor__(:parameters), do: Enum.map(unquote(definition.parameters), &Keyword.fetch!(&1, :name)) def __typed_structor__(:enforced_fields), do: unquote(enforced_fields) for field <- definition.fields do diff --git a/lib/typed_structor.ex b/lib/typed_structor.ex index c2959a4..8db43f8 100644 --- a/lib/typed_structor.ex +++ b/lib/typed_structor.ex @@ -91,7 +91,8 @@ defmodule TypedStructor do # create a lexical scope try do - import TypedStructor, only: [field: 2, field: 3, parameter: 1, plugin: 1, plugin: 2] + import TypedStructor, + only: [field: 2, field: 3, parameter: 1, parameter: 2, plugin: 1, plugin: 2] unquote(register_global_plugins()) @@ -136,6 +137,7 @@ defmodule TypedStructor do defp register_global_plugins do :typed_structor |> Application.get_env(:plugins, []) + |> List.wrap() |> Enum.map(fn {plugin, opts} when is_atom(plugin) and is_list(opts) -> {plugin, opts} @@ -206,14 +208,17 @@ defmodule TypedStructor do fied :number, int # not int() """ - defmacro parameter(name) when is_atom(name) do + defmacro parameter(name, opts \\ []) + + defmacro parameter(name, opts) when is_atom(name) and is_list(opts) do quote do - @__ts_struct_parameters__ unquote(name) + @__ts_struct_parameters__ Keyword.merge(unquote(opts), name: unquote(name)) end end - defmacro parameter(name) do - raise ArgumentError, "expected an atom, got: #{inspect(name)}" + defmacro parameter(name, opts) do + raise ArgumentError, + "name must be an atom and opts must be a list, got: #{inspect(name)} and #{inspect(opts)}" end @doc """ @@ -285,7 +290,15 @@ defmodule TypedStructor do type_name = Keyword.get(@__ts_options__, :type_name, :t) - parameters = Enum.map(@__ts_definition__.parameters, &Macro.var(&1, __MODULE__)) + parameters = + Enum.map( + @__ts_definition__.parameters, + fn parameter -> + parameter + |> Keyword.fetch!(:name) + |> Macro.var(__MODULE__) + end + ) case Keyword.get(@__ts_options__, :type_kind, :type) do :type -> diff --git a/lib/typed_structor/definition.ex b/lib/typed_structor/definition.ex index 389e614..7911142 100644 --- a/lib/typed_structor/definition.ex +++ b/lib/typed_structor/definition.ex @@ -7,7 +7,7 @@ defmodule TypedStructor.Definition do @type t() :: %__MODULE__{ options: Keyword.t(), fields: [Keyword.t()], - parameters: [atom()] + parameters: [Keyword.t()] } defstruct [:options, :fields, :parameters] diff --git a/test/config_test.exs b/test/config_test.exs index e877976..d8534be 100644 --- a/test/config_test.exs +++ b/test/config_test.exs @@ -84,7 +84,7 @@ defmodule ConfigTest do end defp set_plugins_config(plugins) do - previous_value = Application.get_env(:typed_structor, :plugins) + previous_value = Application.get_env(:typed_structor, :plugins, []) Application.put_env(:typed_structor, :plugins, plugins) on_exit(fn -> Application.put_env(:typed_structor, :plugins, previous_value) end) end diff --git a/test/support/test_case.ex b/test/support/test_case.ex index 4d5347c..d462b38 100644 --- a/test/support/test_case.ex +++ b/test/support/test_case.ex @@ -30,7 +30,7 @@ defmodule TypedStructor.TestCase do |> Macro.expand(__CALLER__) |> then(&Module.concat(__CALLER__.module, &1)) - code = Keyword.fetch(options, :do) + code = Keyword.fetch!(options, :do) content = """ @@ -60,14 +60,14 @@ defmodule TypedStructor.TestCase do 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) + File.write!(path, content) + mods = compile_file!(path, dir) + try do fun.() after File.rm!(path) - cleanup_modules([module_name], dir) + cleanup_modules(mods, dir) end end @@ -139,7 +139,7 @@ defmodule TypedStructor.TestCase do module |> Code.Typespec.fetch_types() |> case do - :error -> refute "Failed to fetch types for module #{module}" + :error -> flunk("Failed to fetch types for module #{module}") {:ok, types} -> types end end @@ -150,18 +150,26 @@ defmodule TypedStructor.TestCase do 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}" + _ -> flunk("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}" + docs = + case Code.fetch_docs(module) do + {:docs_v1, _, :elixir, _, _, _, docs} -> docs + {:error, reason} -> flunk("Failed to fetch doc for #{module}: #{inspect(reason)}") + end + + case List.keyfind(docs, {type, name, arity}, 0) do + nil -> + flunk(""" + Failed to fetch doc for #{inspect({type, name, arity})} at #{module}, docs: + #{Enum.map_join(docs, " \n", fn doc -> inspect(elem(doc, 0)) end)} + """) + + {_, _, _, %{"en" => doc}, _} -> + doc end end @@ -174,7 +182,7 @@ defmodule TypedStructor.TestCase do expected_types = format_types(expected) if String.length(String.trim(expected_types)) === 0 do - refute "Expected types are empty: #{inspect(expected)}" + flunk("Expected types are empty: #{inspect(expected)}") end assert expected_types == format_types(actual) diff --git a/test/typed_structor_test.exs b/test/typed_structor_test.exs index 1751942..3a31f26 100644 --- a/test/typed_structor_test.exs +++ b/test/typed_structor_test.exs @@ -379,15 +379,17 @@ defmodule TypedStructorTest do 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 - - typed_structor do - parameter "age" - end - end - end + assert_raise ArgumentError, + ~r|name must be an atom and opts must be a list|, + fn -> + defmodule Struct do + use TypedStructor + + typed_structor do + parameter "age" + end + end + end end end From df4f8c15f30dd9afc3f1e1629b7fdf9bcd3be2ea Mon Sep 17 00:00:00 2001 From: Phil Chen <06fahchen@gmail.com> Date: Sun, 7 Jul 2024 19:39:01 +0800 Subject: [PATCH 4/5] docs: add doc_fields guide (#11) --- guides/plugins/doc_fields.md | 171 ++++++++++++++++++++++++ mix.exs | 1 + test/guides/plugins/doc_fields_test.exs | 39 ++++++ test/support/guide_case.ex | 17 +-- 4 files changed, 220 insertions(+), 8 deletions(-) create mode 100644 guides/plugins/doc_fields.md create mode 100644 test/guides/plugins/doc_fields_test.exs diff --git a/guides/plugins/doc_fields.md b/guides/plugins/doc_fields.md new file mode 100644 index 0000000..78ce7ed --- /dev/null +++ b/guides/plugins/doc_fields.md @@ -0,0 +1,171 @@ +# Add fields docs to the `@typedoc` + +## Implement +```elixir +defmodule Guides.Plugins.DocFields do + @moduledoc """ + The `DocFields` plugin generates documentation for fields and parameters. + Simply add the `:doc` option to the `field` and `parameter` macros to document them. + + ## Example + + use TypedStructor + + typed_structor do + @typedoc \""" + This is a user struct. + \""" + plugin Guides.Plugins.DocFields + + parameter :age, doc: "The age parameter." + + field :name, String.t(), doc: "The name of the user." + field :age, age, doc: "The age of the user." + end + + This will generate the following documentation for you: + + @typedoc \""" + This is a user struct. + + + ## Parameters + + | Name | Description | + |------|-------------| + |`:age` | The age parameter.| + + + ## Fields + + | Name | Type | Description | + |------|------|-------------| + |`:name` | `String.t() \| nil` | The name of the user.| + |`:age` | `age \| nil` | The age of the user.| + \""" + + @type t(age) :: %User{age: age | nil, name: String.t() | nil} + """ + + use TypedStructor.Plugin + + @impl TypedStructor.Plugin + defmacro before_definition(definition, _opts) do + quote do + @typedoc unquote(__MODULE__).__generate_doc__(unquote(definition), @typedoc) + + unquote(definition) + end + end + + def __generate_doc__(_definition, false), do: nil + + def __generate_doc__(definition, typedoc) do + parameters = + Enum.map(definition.parameters, fn parameter -> + name = Keyword.fetch!(parameter, :name) + doc = Keyword.get(parameter, :doc, "*not documented*") + + ["`#{inspect(name)}`", doc] + end) + + parameters_docs = + if length(parameters) > 0 do + """ + ## Parameters + + | Name | Description | + |------|-------------| + #{join_rows(parameters)} + """ + end + + fields = + Enum.map(definition.fields, fn field -> + name = Keyword.fetch!(field, :name) + + type = Keyword.fetch!(field, :type) + + type = + if Keyword.get(field, :enforce, false) or Keyword.has_key?(field, :default) do + Macro.to_string(type) + else + # escape `|` + "#{Macro.to_string(type)} \\| nil" + end + + doc = Keyword.get(field, :doc, "*not documented*") + + ["`#{inspect(name)}`", "`#{type}`", doc] + end) + + fields_docs = + if length(fields) > 0 do + """ + ## Fields + + | Name | Type | Description | + |------|------|-------------| + #{join_rows(fields)} + """ + end + + [parameters_docs, fields_docs] + |> Enum.reject(&is_nil/1) + |> case do + [] -> + typedoc + + docs -> + """ + #{typedoc} + + #{Enum.join(docs, "\n\n")} + """ + end + end + + defp join_rows(rows) do + Enum.map_join(rows, "\n", fn row -> "|" <> Enum.join(row, " | ") <> "|" end) + end +end +``` + +## Usage +```elixir +defmodule User do + @moduledoc false + + use TypedStructor + + typed_structor do + @typedoc """ + This is a user struct. + """ + plugin Guides.Plugins.DocFields + + parameter :age, doc: "The age parameter." + + field :name, String.t(), doc: "The name of the user." + field :age, age, doc: "The age of the user." + end +end +``` + +```elixir +iex> t User.t +@type t(age) :: %User{age: age | nil, name: String.t() | nil} + +This is a user struct. + +## Parameters + +Name | Description +:age | The age parameter. + +## Fields + +Name | Type | Description +:name | String.t() | nil | The name of the user. +:age | age | nil | The age of the user. +``` diff --git a/mix.exs b/mix.exs index fb90ff9..e5e1d00 100644 --- a/mix.exs +++ b/mix.exs @@ -36,6 +36,7 @@ defmodule TypedStructor.MixProject do "guides/plugins/registering_plugins_globally.md", "guides/plugins/accessible.md", "guides/plugins/reflection.md", + "guides/plugins/doc_fields.md", "guides/plugins/type_only_on_ecto_schema.md", "guides/plugins/primary_key_and_timestamps.md", "guides/plugins/derive_jason.md", diff --git a/test/guides/plugins/doc_fields_test.exs b/test/guides/plugins/doc_fields_test.exs new file mode 100644 index 0000000..4e00a20 --- /dev/null +++ b/test/guides/plugins/doc_fields_test.exs @@ -0,0 +1,39 @@ +defmodule Guides.Plugins.DocFieldsTest do + use TypedStructor.TestCase + + @tag :tmp_dir + test "works", ctx do + doc = + with_tmpmodule TestModule, ctx do + unquote( + "doc_fields.md" + |> TypedStructor.GuideCase.extract_code() + |> Code.string_to_quoted!() + ) + after + fetch_doc!(TestModule.User, {:type, :t, 1}) + end + + expected = """ + This is a user struct. + + + ## Parameters + + | Name | Description | + |------|-------------| + |`:age` | The age parameter.| + + + ## Fields + + | Name | Type | Description | + |------|------|-------------| + |`:name` | `String.t() | nil` | The name of the user.| + |`:age` | `age | nil` | The age of the user.| + + """ + + assert expected === doc + end +end diff --git a/test/support/guide_case.ex b/test/support/guide_case.ex index e308589..46d1379 100644 --- a/test/support/guide_case.ex +++ b/test/support/guide_case.ex @@ -6,22 +6,21 @@ defmodule TypedStructor.GuideCase do using opts do guide = Keyword.fetch!(opts, :guide) - file = Path.expand([__DIR__, "../../../", "guides/plugins/", guide]) - - ast = Code.string_to_quoted!(extract_code(file)) + ast = Code.string_to_quoted!(extract_code(guide)) quote do - Code.compiler_options(debug_info: true) + Code.compiler_options(debug_info: true, docs: true) {:module, _module_name, bytecode, _submodule} = unquote(ast) ExUnit.Case.register_module_attribute(__MODULE__, :bytecode) @bytecode bytecode - import unquote(__MODULE__), only: [types: 1] + import unquote(__MODULE__) end end + @spec types(binary()) :: binary() def types(bytecode) when is_binary(bytecode) do bytecode |> TypedStructor.TestCase.fetch_types!() @@ -29,9 +28,11 @@ defmodule TypedStructor.GuideCase do |> Kernel.<>("\n") end - defp extract_code(file) do - content = - File.read!(file) + @spec extract_code(String.t()) :: String.t() + def extract_code(filename) do + file = Path.expand([__DIR__, "../../../", "guides/plugins/", filename]) + + content = File.read!(file) content |> String.split("\n") From da8c9a0b6675a3d97d235bd31f86a2e05d7401f3 Mon Sep 17 00:00:00 2001 From: Phil Chen <06fahchen@gmail.com> Date: Sun, 7 Jul 2024 19:43:58 +0800 Subject: [PATCH 5/5] chore: bump to 0.4.0 --- CHANGELOG.md | 15 +++++++++++++++ README.md | 2 +- mix.exs | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff607ce..9a85d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [0.4.0](https://github.com/elixir-typed-structor/typed_structor/compare/v0.3.0...v0.4.0) (2024-07-07) + + +### ⚠ BREAKING CHANGES + +* store parameters as keywords instead of atoms to retain more information ([#10](https://github.com/elixir-typed-structor/typed_structor/issues/10)) + +### Code Refactoring + +* store parameters as keywords instead of atoms to retain more information ([#10](https://github.com/elixir-typed-structor/typed_structor/issues/10)) ([df5b17e](https://github.com/elixir-typed-structor/typed_structor/commit/df5b17e09cb569cb1c93c470db0e9eb322e429b8)) + +### Features + +* add doc_fields guide ([#11](https://github.com/elixir-typed-structor/typed_structor/pull/11)) + ## [0.3.0](https://github.com/elixir-typed-structor/typed_structor/compare/v0.2.0...v0.3.0) (2024-07-03) diff --git a/README.md b/README.md index 3ab24f2..91c623b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Add `:typed_structor` to the list of dependencies in `mix.exs`: ```elixir def deps do [ - {:typed_structor, "~> 0.3"} + {:typed_structor, "~> 0.4"} ] end ``` diff --git a/mix.exs b/mix.exs index e5e1d00..05d2e95 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule TypedStructor.MixProject do [ app: :typed_structor, description: "TypedStructor is a library for defining structs with types effortlessly.", - version: "0.3.0", + version: "0.4.0", elixir: "~> 1.14", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod,