Skip to content

Commit

Permalink
feat: support :defrecord and :defrecordp and custom definer (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
fahchen authored Sep 27, 2024
1 parent 800ca9e commit cc93185
Show file tree
Hide file tree
Showing 15 changed files with 721 additions and 82 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,21 @@ defmodule HTTPException do
end
```

## Define records related macros

In Elixir, you can use the Record module to define and work with Erlang records,
making interoperability between Elixir and Erlang more seamless.

```elixir
defmodule TypedStructor.User do
use TypedStructor

typed_structor definer: :defrecord, record_name: :user, record_tag: User, enforce: true do
field :name, String.t()
field :age, pos_integer()
end
end
```
## Documentation

To add a `@typedoc` to the struct type, just add the attribute in the typed_structor block:
Expand Down
6 changes: 5 additions & 1 deletion guides/plugins/reflection.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ defmodule Guides.Plugins.Reflection do

enforced_fields =
definition.fields
|> Stream.filter(&Keyword.get(&1, :enforce, false))
|> Stream.filter(fn field ->
Keyword.get_lazy(field, :enforce, fn ->
Keyword.get(definition.options, :enforce, false)
end)
end)
|> Stream.map(&Keyword.fetch!(&1, :name))
|> Enum.to_list()

Expand Down
105 changes: 73 additions & 32 deletions lib/typed_structor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ defmodule TypedStructor do
|> String.split("<!-- MODULEDOC -->", parts: 2)
|> Enum.fetch!(1)

@built_in_definers [
defstruct: TypedStructor.Definer.Defstruct,
defexception: TypedStructor.Definer.Defexception,
defrecord: TypedStructor.Definer.Defrecord,
defrecordp: TypedStructor.Definer.Defrecordp
]

defmacro __using__(_opts) do
quote do
import TypedStructor, only: [typed_structor: 1, typed_structor: 2]
Expand All @@ -28,24 +35,28 @@ defmodule TypedStructor do
The available definers are:
- `:defstruct`, which defines a struct and a type for a given definition
- `:defexception`, which defines an exception and a type for a given definition
- `:defrecord`, which defines record macros and a type for a given definition
- `:defrecordp`, which defines private record macros and a type for a given definition
### `:defstruct` options
* `:define_struct` - if `false`, the type will be defined, but the struct will not be defined. Defaults to `true`.
#{TypedStructor.Definer.Defstruct.__additional_options__()}
### `:defexception` options
* `:define_struct` - if `false`, the type will be defined, but the exception struct will not be defined. Defaults to `true`.
#{TypedStructor.Definer.Defexception.__additional_options__()}
### `:defrecord` and `:defrecordp` options
#{TypedStructor.Definer.Defrecord.__additional_options__()}
### custom definer
defmodule MyStruct do
# you must require the definer module to use its define/1 macro
require MyDefiner
use TypedStructor
typed_structor definer: &MyDefiner.define/1 do
typed_structor definer: MyDefiner do
field :name, String.t()
field :age, integer()
end
Expand Down Expand Up @@ -91,7 +102,7 @@ defmodule TypedStructor do
defmacro typed_structor(options \\ [], do: block) when is_list(options) do
case Keyword.pop(options, :module) do
{nil, options} ->
__typed_structor__(__CALLER__.module, options, block)
__typed_structor__(__CALLER__, options, block)

{module, options} ->
quote do
Expand All @@ -106,16 +117,18 @@ defmodule TypedStructor do
end
end

defp __typed_structor__(mod, options, block) do
Module.register_attribute(mod, :__ts_options__, accumulate: false)
Module.register_attribute(mod, :__ts_struct_fields__, accumulate: true)
Module.register_attribute(mod, :__ts_struct_parameters__, accumulate: true)
Module.register_attribute(mod, :__ts_struct_plugins__, accumulate: true)
Module.register_attribute(mod, :__ts_definition____, accumulate: false)
defp __typed_structor__(caller, options, block) when is_list(options) do
case fetch_definer!(caller, options) do
:error -> :ok
{:ok, definer} -> Module.put_attribute(caller.module, :__ts_definer__, definer)
end

quote do
@__ts_options__ unquote(options)
Module.register_attribute(caller.module, :__ts_struct_fields__, accumulate: true)
Module.register_attribute(caller.module, :__ts_struct_parameters__, accumulate: true)
Module.register_attribute(caller.module, :__ts_struct_plugins__, accumulate: true)
Module.register_attribute(caller.module, :__ts_definition____, accumulate: false)

quote do
# create a lexical scope
try do
import TypedStructor,
Expand All @@ -132,15 +145,14 @@ defmodule TypedStructor do
try do
definition =
TypedStructor.__call_plugins_before_definitions__(%TypedStructor.Definition{
options: @__ts_options__,
options: unquote(options),
fields: Enum.reverse(@__ts_struct_fields__),
parameters: Enum.reverse(@__ts_struct_parameters__)
})

@__ts_definition__ definition
after
# cleanup
Module.delete_attribute(__MODULE__, :__ts_options__)
Module.delete_attribute(__MODULE__, :__ts_struct_fields__)
Module.delete_attribute(__MODULE__, :__ts_struct_parameters__)
end
Expand All @@ -154,10 +166,30 @@ defmodule TypedStructor do
# cleanup
Module.delete_attribute(__MODULE__, :__ts_struct_plugins__)
Module.delete_attribute(__MODULE__, :__ts_definition__)
Module.delete_attribute(__MODULE__, :__ts_definer__)
end
end
end

defp fetch_definer!(caller, options) when is_list(options) do
case Keyword.fetch(options, :definer) do
:error ->
:error

{:ok, definer} ->
case Macro.expand(definer, caller) do
built_in_or_mod when is_atom(built_in_or_mod) ->
{:ok, built_in_or_mod}

other ->
raise ArgumentError, """
Definer must be one of :defstruct, :defexception, :defrecord, :defrecordp or a module that defines a `define/1` macro,
got: #{inspect(other)}
"""
end
end
end

# register global plugins
defp register_global_plugins do
:typed_structor
Expand Down Expand Up @@ -219,7 +251,7 @@ defmodule TypedStructor do
options = Keyword.merge(options, name: name, type: Macro.escape(type))

quote do
@__ts_struct_fields__ Keyword.merge(@__ts_options__, unquote(options))
@__ts_struct_fields__ unquote(options)
end
end

Expand Down Expand Up @@ -272,21 +304,30 @@ defmodule TypedStructor do
end

defmacro __define__(definition) do
quote bind_quoted: [definition: definition] do
case Keyword.get(definition.options, :definer, :defstruct) do
:defstruct ->
require TypedStructor.Definer.Defstruct
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
TypedStructor.Definer.Defstruct.define(definition)

:defexception ->
require TypedStructor.Definer.Defexception
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
TypedStructor.Definer.Defexception.define(definition)

fun when is_function(fun) ->
then(definition, fun)
definer = Module.get_attribute(__CALLER__.module, :__ts_definer__, :defstruct)
definer_mod = Keyword.get(@built_in_definers, definer, definer)

quote do
case Keyword.fetch(unquote(definition).options, :definer) do
:error ->
:ok

{:ok, unquote(definer)} ->
:ok

{:ok, other} ->
IO.warn("""
The definer option set in the `typed_structor` block is different from the definer option in the definition.
We will ignore the definer option in the definition and use the one set in the `typed_structor` block.
Note: The definer option in the definition may be changed by a plugin.
The effective definer is: #{inspect(unquote(definer))}, the ignored definer from the definition is: #{inspect(other)}.
""")
end

require unquote(definer_mod)
unquote(definer_mod).define(unquote(definition))
end
end

Expand Down
13 changes: 10 additions & 3 deletions lib/typed_structor/definer/defexception.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
defmodule TypedStructor.Definer.Defexception do
additional_options = """
* `:define_struct` - if `false`, the type will be defined, but the struct will not be defined. Defaults to `true`.
"""

@moduledoc """
A definer to define an exception and a type for a given definition.
## Additional options for `typed_structor`
* `:define_struct` - if `false`, the type will be defined, but the struct will not be defined. Defaults to `true`.
#{additional_options}
## Usage
Expand All @@ -18,6 +22,7 @@ defmodule TypedStructor.Definer.Defexception do
"""

alias TypedStructor.Definer.Defstruct
alias TypedStructor.Definer.Utils

@doc """
Defines an exception and a type for a given definition.
Expand All @@ -35,12 +40,14 @@ defmodule TypedStructor.Definer.Defexception do
defmacro __exception_ast__(definition) do
quote bind_quoted: [definition: definition] do
if Keyword.get(definition.options, :define_struct, true) do
{fields, enforce_keys} =
Defstruct.__extract_fields_and_enforce_keys__(definition)
{fields, enforce_keys} = Utils.fields_and_enforce_keys(definition)

@enforce_keys Enum.reverse(enforce_keys)
defexception fields
end
end
end

@doc false
def __additional_options__, do: unquote(additional_options)
end
Loading

0 comments on commit cc93185

Please sign in to comment.