Skip to content

Commit

Permalink
refactor!: store parameters as keywords instead of atoms to retain mo…
Browse files Browse the repository at this point in the history
…re information (#10)
  • Loading branch information
fahchen authored Jul 7, 2024
1 parent 5906967 commit df5b17e
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 34 deletions.
2 changes: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -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}"],
Expand Down
2 changes: 1 addition & 1 deletion guides/plugins/reflection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 19 additions & 6 deletions lib/typed_structor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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 """
Expand Down Expand Up @@ -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 ->
Expand Down
2 changes: 1 addition & 1 deletion lib/typed_structor/definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion test/config_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 23 additions & 15 deletions test/support/test_case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
"""
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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)
Expand Down
20 changes: 11 additions & 9 deletions test/typed_structor_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit df5b17e

Please sign in to comment.