Current section
Files
Jump to
Current section
Files
lib/type_struct.ex
defmodule TypeStruct do
@moduledoc ~S"""
TypeStruct provides a concise syntax for defining structs and their types.
"""
# Elixir built-in types. Last updated for Elixir 1.4.2.
# See https://hexdocs.pm/elixir/typespecs.html.
# `:any` represents any arity (0..infinity).
@builtin_types [
any: 0,
none: 0,
atom: 0,
map: 0,
pid: 0,
port: 0,
reference: 0,
struct: 0,
tuple: 0,
float: 0,
integer: 0,
neg_integer: 0,
non_neg_integer: 0,
pos_integer: 0,
list: 1,
nonempty_list: 1,
maybe_improper_list: :any,
nonempty_improper_list: :any,
nonempty_maybe_improper_list: :any,
term: 0,
arity: 0,
as_boolean: 1,
binary: 0,
bitstring: 0,
boolean: 0,
byte: 0,
char: 0,
charlist: 0,
fun: 0,
identifier: 0,
iodata: 0,
iolist: 0,
keyword: 0,
keyword: 1,
list: 0,
nonempty_list: 0,
maybe_improper_list: 0,
nonempty_maybe_improper_list: 0,
mfa: 0,
module: 0,
no_return: 0,
node: 0,
number: 0,
struct: 0,
timeout: 0
]
defmacro __using__(_opts) do
quote do
import TypeStruct
import Kernel, except: [defstruct: 1]
end
end
# Possible uses:
# - `defstruct x: integer, y: integer`
defmacro defstruct(fields)
defmacro defstruct(keywords),
do: do_defstruct(keywords, default_type_definition())
# Possible uses:
# - `defstruct Point, x: integer, y: integer`
# - `defstruct type(t), x: integer, y: integer`
defmacro defstruct(alias_or_type_definition, fields)
defmacro defstruct({:__aliases__, _meta, _args} = alias, keywords) do
do_defmodule_defstruct(alias, __CALLER__,
keywords, default_type_definition())
end
defmacro defstruct(quoted_type, keywords) do
do_defstruct(keywords, parse_quoted_type(quoted_type))
end
# Possible uses:
# - `defstruct Point, type(t), x: integer, y: integer`
defmacro defstruct(alias, type_definition, fields)
defmacro defstruct(alias, quoted_type, keywords) do
do_defmodule_defstruct(alias, __CALLER__,
keywords, parse_quoted_type(quoted_type))
end
# Convenience function to keep the macro definitions tidy.
defp do_defmodule_defstruct(alias, caller, keywords, type_definition) do
alias_types? = caller.module != nil
quoted_defstruct =
do_defstruct(keywords, type_definition, alias_types?, caller.module)
quoted_alias_block = create_quoted_alias_block(caller.aliases)
quote do
typedoc =
with module when module != nil <- __MODULE__,
{_line, typedoc} <- Module.delete_attribute(module, :typedoc) do
typedoc
else
nil -> nil
end
defmodule unquote(alias) do
unquote(quoted_alias_block)
@typedoc typedoc
unquote(quoted_defstruct)
end
end
end
defp do_defstruct(keywords, type_definition),
do: do_defstruct(keywords, type_definition, false, nil)
defp do_defstruct(keywords, type_definition, alias_types?, caller_alias) do
model =
keywords
|> parse_keyword_list
|> maybe_map_module_to_types(alias_types?, caller_alias)
defstruct_args =
for {key, {_, _, default}} <- model, do: {key, default}
enforce_keys_args =
Enum.filter_map(model, &(&1 |> elem(1) |> elem(1)), &elem(&1, 0))
quoted_type = create_quoted_type(type_definition, model)
quote do
# Must be above struct definition.
@enforce_keys unquote(enforce_keys_args)
Kernel.defstruct(unquote(defstruct_args))
# Now we set the @type/@typep/@opaque attribute.
unquote(quoted_type)
end
end
defp maybe_map_module_to_types(model, false, _module), do: model
defp maybe_map_module_to_types(model, true, module) do
for {key, {type, required, default}} <- model do
type = maybe_prepend_module(type, module)
{key, {type, required, default}}
end
end
# Takes a quoted keyword list and returns a model in
# the format `[key: {type, required?, default}, ...]`
# where `type` is a quoted representation of the
# field's type, `required?` is a boolean, and `default`
# is a quoted representation of the field's default
# value.
defp parse_keyword_list(keywords) do
Enum.map keywords, fn({key, value}) ->
{key, parse_keyword_value(value)}
end
end
defp parse_keyword_value({:\\, _meta, [type, default]}),
do: {type, false, default}
defp parse_keyword_value(type),
do: {type, true, nil}
# Type definitions are described using the format
# `{type_attribute, type_name}` where `type_attribute`
# is an atom, and `type_name` is a quoted
# representation of the type's name.
defp parse_quoted_type({:type, _meta, [name]}), do: {:type, name}
defp parse_quoted_type({:typep, _meta, [name]}), do: {:typep, name}
defp parse_quoted_type({:opaque, _meta, [name]}), do: {:opaque, name}
defp default_type_definition, do: {:type, {:t, [], Elixir}}
defp create_quoted_type({type_attribute, quoted_type_name}, model) do
type_alias_args =
Enum.map(model, fn({key, {type, _, _}}) -> {key, type} end)
quoted_struct_type =
{:%, [], [{:__MODULE__, [], Elixir}, {:%{}, [], type_alias_args}]}
create_quoted_type(type_attribute, quoted_type_name, quoted_struct_type)
end
defp create_quoted_type(:type, quoted_name, quoted_struct_type),
do: quote do: @type unquote(quoted_name) :: unquote(quoted_struct_type)
defp create_quoted_type(:typep, quoted_name, quoted_struct_type),
do: quote do: @typep unquote(quoted_name) :: unquote(quoted_struct_type)
defp create_quoted_type(:opaque, quoted_name, quoted_struct_type),
do: quote do: @opaque unquote(quoted_name) :: unquote(quoted_struct_type)
# Takes a `Macro.Env.aliases` list, aka
# `[{alias, module]}`, and returns a quoted block
# of `alias/2` calls.
defp create_quoted_alias_block(aliases) do
for {aliased, actual} <- aliases do
quote do: alias unquote(actual), as: unquote(aliased)
end
end
# Takes a quoted type and a module. Walks the quoted
# type and prepends the module to any type that isn't
# a built-in.
defp maybe_prepend_module(list, module) when is_list(list) do
Enum.map(list, &maybe_prepend_module(&1, module))
end
defp maybe_prepend_module({atom, meta, args}, module) when is_atom(atom) do
with true <- possible_type_name?(atom),
arity <- quoted_args_arity(args),
false <- is_builtin?(atom, arity) do
args = if args, do: maybe_prepend_module(args, module), else: []
module_atom_parts = split_module_into_atoms(module)
{{:".", [], [{:__aliases__, [], module_atom_parts}, atom]}, [], args}
else
_ -> {atom, meta, maybe_prepend_module(args, module)}
end
end
defp maybe_prepend_module({node, meta, args}, module) when is_tuple(node) do
{maybe_prepend_module(node, module), meta, args}
end
defp maybe_prepend_module(value, _module) do
value
end
defp possible_type_name?(atom) do
atom |> Atom.to_string |> String.match?(~r/^[a-z][a-zA-Z0-9_]*[?!]{0,1}$/)
end
defp quoted_args_arity(nil), do: 0
defp quoted_args_arity(list), do: length(list)
defp is_builtin?(atom, arity) do
Enum.any? @builtin_types, fn
{type, :any} -> type == atom
{type, type_arity} -> type == atom && type_arity == arity
end
end
# Converts `Foo.Bar` to `[:Foo, :Bar]` (for example).
defp split_module_into_atoms(module),
do: module |> Module.split |> Enum.map(&String.to_atom/1)
end