Skip to content

Instantly share code, notes, and snippets.

@doorgan
Last active April 10, 2021 16:31
Show Gist options
  • Save doorgan/abc126798b43f5ba47351917379240af to your computer and use it in GitHub Desktop.
Save doorgan/abc126798b43f5ba47351917379240af to your computer and use it in GitHub Desktop.
Typed struct macro
defmodule Main do
import TypedStruct
typedstruct do
field :id, integer(), enforced?: true
field :body, String.t(), default: "Foo"
field :count, integer()
field :metadata, map(), default: %{foo: :bar}, redacted?: true
end
typedstruct Z do
field :id, integer(), enforced?: true
field :body, String.t(), default: "Foo"
field :count, integer()
end
end
defmodule TypedStruct do
defmodule FieldSpec do
defstruct [:name, :typespec, :default, :enforced?, :redacted?]
def new(spec) when is_map(spec) do
{default, _} = Code.eval_quoted(spec.default)
%__MODULE__{
name: spec.name,
typespec: Macro.to_string(spec.typespec),
default: default,
enforced?: spec.enforced?,
redacted?: spec.redacted?
}
end
end
alias TypedStruct.FieldSpec
defmacro typedstruct(do: ast) do
do_typedstruct(ast)
end
defmacro typedstruct({:__aliases__, _, _} = module, do: ast) do
quote location: :keep do
defmodule unquote(module) do
unquote(do_typedstruct(ast))
end
end
end
defp do_typedstruct(ast) do
fields_ast =
case ast do
{:__block__, _, fields} -> fields
field -> [field]
end
specs = Enum.map(fields_ast, &to_spec/1)
typespecs =
Enum.map(specs, fn
%{name: name, typespec: typespec, enforced?: false} ->
# Builds `typespec | nil`
{name, {:|, [], [typespec, :nil]}}
%{name: name, typespec: typespec} ->
{name, typespec}
end)
fields =
Enum.map(specs, fn
%{name: name, default: nil} -> name
%{name: name, default: default} -> {name, default}
end)
enforced_fields =
for spec <- specs, spec.enforced? do
spec.name
end
redacted_fields =
for spec <- specs, spec.redacted? do
spec.name
end
attrs_typespec =
for spec <- specs do
if spec.enforced? do
{spec.name, spec.typespec}
else
# Builds `{optional(name), typespec | nil}` which
# turns into `optional(name) => typespec | nil` when
# `unquote_splicing` in a map typespec
{
{:optional, [], [spec.name]},
{:|, [], [spec.typespec, :nil]}
}
end
end
quote location: :keep do
@derive {Inspect, except: unquote(redacted_fields)}
@type t :: %__MODULE__{unquote_splicing(typespecs)}
@enforce_keys unquote(enforced_fields)
defstruct unquote(fields)
@spec new(attrs :: %{unquote_splicing(attrs_typespec)}) :: t
def new(attrs) when is_map(attrs) do
struct(__MODULE__, attrs)
end
def __typedstruct__(:specs) do
unquote(
specs
|> Enum.map(&FieldSpec.new/1)
|> Macro.escape()
)
end
end
end
defp to_spec({:field, _metadata, [name, typespec]}) do
to_spec({:field, [], [name, typespec, []]})
end
defp to_spec({:field, _metadata, [name, typespec, opts]}) do
default = Keyword.get(opts, :default)
enforced? = Keyword.get(opts, :enforced?, false)
redacted? = Keyword.get(opts, :redacted?, false)
%{
name: name,
typespec: typespec,
default: default,
enforced?: enforced?,
redacted?: redacted?
}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment