Last active
April 10, 2021 16:31
-
-
Save doorgan/abc126798b43f5ba47351917379240af to your computer and use it in GitHub Desktop.
Typed struct macro
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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