Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support :defrecord and :defrecordp and custom definer #21

Merged
merged 7 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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