617 lines
17 KiB
Elixir
617 lines
17 KiB
Elixir
defmodule Record do
|
|
@moduledoc """
|
|
Functions to define Elixir records
|
|
"""
|
|
|
|
@doc """
|
|
Extract record information from an Erlang file and
|
|
return the fields as a list of tuples.
|
|
|
|
## Examples
|
|
|
|
defrecord FileInfo, Record.extract(:file_info, from_lib: "kernel/include/file.hrl")
|
|
|
|
"""
|
|
def extract(name, opts) do
|
|
Record.Extractor.retrieve(name, opts)
|
|
end
|
|
|
|
@doc """
|
|
Main entry point for records definition. It defines a module
|
|
with the given `name` and the fields specified in `modules`.
|
|
This is invoked directly by `Kernel.defrecord`, so check it
|
|
for more information and documentation.
|
|
"""
|
|
def defrecord(name, values, opts) do
|
|
block = Keyword.get(opts, :do, nil)
|
|
opts = Keyword.delete(opts, :do)
|
|
|
|
quote do
|
|
defmodule unquote(name) do
|
|
@moduledoc false
|
|
Record.deffunctions(__ENV__, unquote(values), unquote(opts))
|
|
unquote(block)
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Defines record functions skipping the module definition.
|
|
This is called directly by `defrecord`. It expects the
|
|
module environment, the module values and a keyword list
|
|
of options.
|
|
|
|
## Examples
|
|
|
|
defmodule CustomRecord do
|
|
Record.deffunctions __ENV__, [:name, :age]
|
|
end
|
|
|
|
"""
|
|
def deffunctions(env, values, opts // []) do
|
|
values = lc value inlist values, do: convert_value(value)
|
|
escaped = Macro.escape(values)
|
|
extensions = Keyword.get(opts, :extensions, Record.Extensions)
|
|
|
|
contents = [
|
|
reflection(escaped),
|
|
initializer(escaped),
|
|
indexes(escaped),
|
|
readers(values, 1, []),
|
|
conversions(values),
|
|
writers(values, 1, []),
|
|
updater(values),
|
|
extensions(values, 1, [], extensions)
|
|
]
|
|
|
|
# Special case for bootstraping purposes
|
|
if env == Macro.Env do
|
|
:elixir_module.eval_quoted(env, contents, [], [])
|
|
else
|
|
contents = [quote(do: @__record__ unquote(escaped))|contents]
|
|
Module.eval_quoted(env, contents)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Defines three macros for reading and writing records values.
|
|
These macros are private to the current module and are
|
|
basically a simple mechanism for manipulating tuples when
|
|
there isn't an interest in exposing the record as a whole.
|
|
In some ways, it is similar to Erlang records, since it is
|
|
only available at compilation time.
|
|
|
|
## Examples
|
|
|
|
defmodule CustomModule do
|
|
Record.defmacros __ENV__, :_user, [:name, :age]
|
|
|
|
def new(name, age) do
|
|
_user(name: name, age: age)
|
|
end
|
|
|
|
def name(user, name) do
|
|
_user(user, name: name)
|
|
end
|
|
|
|
def age(user) do
|
|
_user(user, :age)
|
|
end
|
|
|
|
def to_keywords(user) do
|
|
_user(user)
|
|
end
|
|
|
|
def name_and_age(user) do
|
|
_user(user, [:name, :age])
|
|
end
|
|
|
|
def age_and_name(user) do
|
|
_user(user, [:age, :name])
|
|
end
|
|
|
|
end
|
|
|
|
"""
|
|
def defmacros(env, name, values) do
|
|
escaped = lc value inlist values, do: Macro.escape(convert_value(value))
|
|
|
|
contents = quote do
|
|
defmacrop unquote(name).(record) when is_tuple(record) do
|
|
Record.to_keywords(__CALLER__, __MODULE__, unquote(escaped), record)
|
|
end
|
|
|
|
defmacrop unquote(name).(args) do
|
|
Record.access(__CALLER__, __MODULE__, unquote(escaped), args)
|
|
end
|
|
|
|
defmacrop unquote(name).(record, key) when is_atom(key) do
|
|
Record.get(__CALLER__, __MODULE__, unquote(escaped), record, key)
|
|
end
|
|
|
|
defmacrop unquote(name).(record, args) do
|
|
Record.dispatch(__CALLER__, __MODULE__, unquote(escaped), record, args)
|
|
end
|
|
end
|
|
|
|
Module.eval_quoted(env, contents)
|
|
end
|
|
|
|
defp convert_value(atom) when is_atom(atom) do
|
|
{ atom, nil }
|
|
end
|
|
|
|
defp convert_value(other), do: other
|
|
|
|
# Implements the access macro used by records.
|
|
# It returns a quoted expression that defines
|
|
# a record or a match in case the record is
|
|
# inside a match.
|
|
@doc false
|
|
def access(caller, atom, fields, keyword) do
|
|
unless is_orddict(keyword) do
|
|
raise "expected contents inside brackets to be a Keyword"
|
|
end
|
|
|
|
in_match = caller.in_match?
|
|
|
|
has_underscore_value = Keyword.has_key?(keyword, :_)
|
|
underscore_value = Keyword.get(keyword, :_, { :_, 0, nil })
|
|
keyword = Keyword.delete keyword, :_
|
|
|
|
iterator = fn({field, default}, each_keyword) ->
|
|
new_fields =
|
|
case Keyword.has_key?(each_keyword, field) do
|
|
true -> Keyword.get(each_keyword, field)
|
|
false ->
|
|
case in_match or has_underscore_value do
|
|
true -> underscore_value
|
|
false -> Macro.escape(default)
|
|
end
|
|
end
|
|
|
|
{ new_fields, Keyword.delete(each_keyword, field) }
|
|
end
|
|
|
|
{ match, remaining } = :lists.mapfoldl(iterator, keyword, fields)
|
|
|
|
case remaining do
|
|
[] -> { :{}, caller.line, [atom|match] }
|
|
_ ->
|
|
keys = lc { key, _ } inlist remaining, do: key
|
|
raise "record #{inspect atom} does not have the keys: #{inspect keys}"
|
|
end
|
|
end
|
|
|
|
# Dispatch the call to either update or to_list depending on the args given.
|
|
@doc false
|
|
def dispatch(caller, atom, fields, record, args) do
|
|
if is_orddict(args) do
|
|
update(caller, atom, fields, record, args)
|
|
else
|
|
to_list(caller, atom, fields, record, args)
|
|
end
|
|
end
|
|
|
|
# Implements the update macro defined by defmacros.
|
|
# It returns a quoted expression that represents
|
|
# the access given by the keywords.
|
|
@doc false
|
|
defp update(caller, atom, fields, var, keyword) do
|
|
unless is_orddict(keyword) do
|
|
raise "expected contents inside brackets to be a Keyword"
|
|
end
|
|
|
|
if caller.in_match? do
|
|
raise "cannot invoke update style macro inside match context"
|
|
end
|
|
|
|
Enum.reduce keyword, var, fn({ key, value }, acc) ->
|
|
index = find_index(fields, key, 0)
|
|
if index do
|
|
quote do
|
|
:erlang.setelement(unquote(index + 2), unquote(acc), unquote(value))
|
|
end
|
|
else
|
|
raise "record #{inspect atom} does not have the key: #{inspect key}"
|
|
end
|
|
end
|
|
end
|
|
|
|
# Implements the get macro defined by defmacros.
|
|
# It returns a quoted expression that represents
|
|
# getting the value of a given field.
|
|
@doc false
|
|
def get(_caller, atom, fields, var, key) do
|
|
index = find_index(fields, key, 0)
|
|
if index do
|
|
quote do
|
|
:erlang.element(unquote(index + 2), unquote(var))
|
|
end
|
|
else
|
|
raise "record #{inspect atom} does not have the key: #{inspect key}"
|
|
end
|
|
end
|
|
|
|
# Implements to_keywords macro defined by defmacros.
|
|
# It returns a quoted expression that represents
|
|
# converting record to keywords list.
|
|
@doc false
|
|
def to_keywords(_caller, _atom, fields, record) do
|
|
Enum.map Keyword.from_enum(fields),
|
|
fn({key, _default}) ->
|
|
index = find_index(fields, key, 0)
|
|
quote do
|
|
{unquote(key), :erlang.element(unquote(index + 2), unquote(record))}
|
|
end
|
|
end
|
|
end
|
|
|
|
# Implements to_list macro defined by defmacros.
|
|
# It returns a quoted expression that represents
|
|
# extracting given fields from record.
|
|
@doc false
|
|
defp to_list(_caller, atom, fields, record, keys) do
|
|
Enum.map keys,
|
|
fn(key) ->
|
|
index = find_index(fields, key, 0)
|
|
if index do
|
|
quote do: :erlang.element(unquote(index + 2), unquote(record))
|
|
else
|
|
raise "record #{inspect atom} does not have the key: #{inspect key}"
|
|
end
|
|
end
|
|
end
|
|
|
|
defp is_orddict(list) when is_list(list), do: :lists.all(is_orddict_tuple(&1), list)
|
|
defp is_orddict(_), do: false
|
|
|
|
defp is_orddict_tuple({ x, _ }) when is_atom(x), do: true
|
|
defp is_orddict_tuple(_), do: false
|
|
|
|
# Define __record__/1 and __record__/2 as reflection functions
|
|
# that returns the record names and fields.
|
|
#
|
|
# Note that fields are *not* keywords. They are in the same
|
|
# order as given as parameter and reflects the order of the
|
|
# fields in the tuple.
|
|
#
|
|
# ## Examples
|
|
#
|
|
# defrecord FileInfo, atime: nil, mtime: nil
|
|
#
|
|
# FileInfo.__record__(:name) #=> FileInfo
|
|
# FileInfo.__record__(:fields) #=> [atime: nil, mtime: nil]
|
|
#
|
|
defp reflection(values) do
|
|
quote do
|
|
def __record__(kind, _), do: __record__(kind)
|
|
def __record__(:name), do: __MODULE__
|
|
def __record__(:fields), do: unquote(values)
|
|
end
|
|
end
|
|
|
|
# Define initializers methods. For a declaration like:
|
|
#
|
|
# defrecord FileInfo, atime: nil, mtime: nil
|
|
#
|
|
# It will define three methods:
|
|
#
|
|
# def new() do
|
|
# new([])
|
|
# end
|
|
#
|
|
# def new([]) do
|
|
# { FileInfo, nil, nil }
|
|
# end
|
|
#
|
|
# def new(opts) do
|
|
# { FileInfo, Keyword.get(opts, :atime), Keyword.get(opts, :mtime) }
|
|
# end
|
|
#
|
|
defp initializer(values) do
|
|
defaults = lc { _, value } inlist values, do: value
|
|
|
|
# For each value, define a piece of code that will receive
|
|
# an ordered dict of options (opts) and it will try to fetch
|
|
# the given key from the ordered dict, falling back to the
|
|
# default value if one does not exist.
|
|
selective = lc { k, v } inlist values do
|
|
quote do: Keyword.get(opts, unquote(k), unquote(v))
|
|
end
|
|
|
|
quote do
|
|
def new(), do: new([])
|
|
def new([]), do: { __MODULE__, unquote_splicing(defaults) }
|
|
def new(opts) when is_list(opts), do: { __MODULE__, unquote_splicing(selective) }
|
|
def new(tuple) when is_tuple(tuple), do: :erlang.setelement(1, tuple, __MODULE__)
|
|
end
|
|
end
|
|
|
|
# Define method to get index of a given key.
|
|
#
|
|
# Useful if you need to know position of the key for such applications as:
|
|
# - ets
|
|
# - mnesia
|
|
#
|
|
# For a declaration like:
|
|
#
|
|
# defrecord FileInfo, atime: nil, mtime: nil
|
|
#
|
|
# It will define following method:
|
|
#
|
|
# def __index__(:atime), do: 2
|
|
# def __index__(:mtime), do: 3
|
|
# def __index__(_), do: nil
|
|
#
|
|
defp indexes(values) do
|
|
quoted = lc { k, _ } inlist values do
|
|
index = find_index(values, k, 0)
|
|
quote do
|
|
def __index__(unquote(k)), do: unquote(index + 1)
|
|
end
|
|
end
|
|
quote do
|
|
unquote(quoted)
|
|
def __index__(_), do: nil
|
|
def __index__(key, _), do: __index__(key)
|
|
end
|
|
end
|
|
|
|
# Define converters method(s). For a declaration like:
|
|
#
|
|
# defrecord FileInfo, atime: nil, mtime: nil
|
|
#
|
|
# It will define one method, to_keywords, which will return a Keyword
|
|
#
|
|
# [atime: nil, mtime: nil]
|
|
#
|
|
defp conversions(values) do
|
|
sorted = lc { k, _ } inlist values do
|
|
index = find_index(values, k, 0)
|
|
{ k, quote(do: :erlang.element(unquote(index + 2), record)) }
|
|
end
|
|
|
|
quote do
|
|
def to_keywords(record) do
|
|
unquote(:orddict.from_list(sorted))
|
|
end
|
|
end
|
|
end
|
|
|
|
defp find_index([{ k, _ }|_], k, i), do: i
|
|
defp find_index([{ _, _ }|t], k, i), do: find_index(t, k, i + 1)
|
|
defp find_index([], _k, _i), do: nil
|
|
|
|
# Implement readers. For a declaration like:
|
|
#
|
|
# defrecord FileInfo, atime: nil, mtime: nil
|
|
#
|
|
# It will define four methods:
|
|
#
|
|
# def :atime.(record) do
|
|
# elem(record, 1)
|
|
# end
|
|
#
|
|
# def :mtime.(record) do
|
|
# elem(record, 2)
|
|
# end
|
|
#
|
|
defp readers([{ key, _default }|t], i, acc) do
|
|
contents = quote do
|
|
def unquote(key).(record) do
|
|
:erlang.element(unquote(i + 1), record)
|
|
end
|
|
end
|
|
|
|
readers(t, i + 1, [contents | acc])
|
|
end
|
|
|
|
defp readers([], _i, acc), do: acc
|
|
|
|
# Implement writers. For a declaration like:
|
|
#
|
|
# defrecord FileInfo, atime: nil, mtime: nil
|
|
#
|
|
# It will define four methods:
|
|
#
|
|
# def :atime.(value, record) do
|
|
# setelem(record, 1, value)
|
|
# end
|
|
#
|
|
# def :mtime.(record) do
|
|
# setelem(record, 2, value)
|
|
# end
|
|
#
|
|
defp writers([{ key, _default }|t], i, acc) do
|
|
contents = quote do
|
|
def unquote(key).(value, record) do
|
|
:erlang.setelement(unquote(i + 1), record, value)
|
|
end
|
|
end
|
|
|
|
writers(t, i + 1, [contents | acc])
|
|
end
|
|
|
|
defp writers([], _i, acc), do: acc
|
|
|
|
# Defines update/2
|
|
defp updater(values) do
|
|
fields =
|
|
lc {key, _default} inlist values do
|
|
quote do
|
|
Keyword.get(keywords,
|
|
unquote(key),
|
|
elem(record, unquote(find_index(values, key, 1))))
|
|
end
|
|
end
|
|
contents = {:{}, 0, [(quote do: __MODULE__)|fields]}
|
|
quote do
|
|
def update(keywords, record) do
|
|
unquote(contents)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Defines extra functions from the definition.
|
|
defp extensions([{ key, default }|t], i, acc, extensions) do
|
|
functions = extensions.functions_for(key, default, i)
|
|
extensions(t, i + 1, [functions | acc], extensions)
|
|
end
|
|
|
|
defp extensions([], _i, acc, _), do: acc
|
|
end
|
|
|
|
defmodule Record.Extractor do
|
|
@moduledoc false
|
|
|
|
# Retrieve a record definition from an Erlang file using
|
|
# the same lookup as the *include* attribute from Erlang modules.
|
|
def retrieve(name, from: string) do
|
|
file = to_char_list(string)
|
|
|
|
case :code.where_is_file(file) do
|
|
:non_existing -> realfile = file
|
|
realfile -> nil
|
|
end
|
|
|
|
retrieve_record(name, realfile)
|
|
end
|
|
|
|
# Retrieve a record definition from an Erlang file using
|
|
# the same lookup as the *include_lib* attribute from Erlang modules.
|
|
def retrieve(name, from_lib: file) do
|
|
[app|path] = :filename.split(to_char_list(file))
|
|
case :code.lib_dir(to_char_list(app)) do
|
|
{ :error, _ } ->
|
|
raise ArgumentError, "Lib file #{to_binary(file)} could not be found"
|
|
libpath ->
|
|
retrieve_record name, :filename.join([libpath|path])
|
|
end
|
|
end
|
|
|
|
# Retrieve the record with the given name from the given file
|
|
defp retrieve_record(name, file) do
|
|
records = retrieve_from_file(file)
|
|
if record = List.keyfind(records, name, 0) do
|
|
parse_record(record)
|
|
else
|
|
raise ArgumentError, "No record #{name} found at #{to_binary(file)}"
|
|
end
|
|
end
|
|
|
|
# Parse the given file and retrieve all existent records.
|
|
defp retrieve_from_file(file) do
|
|
lc { :attribute, _, :record, record } inlist read_file(file), do: record
|
|
end
|
|
|
|
# Read a file and return its abstract syntax form that also
|
|
# includes record and other preprocessor modules. This is done
|
|
# by using Erlang's epp_dodger.
|
|
defp read_file(file) do
|
|
case :epp_dodger.quick_parse_file(file) do
|
|
{ :ok, form } ->
|
|
form
|
|
other ->
|
|
raise "Error parsing file #{to_binary(file)}, got: #{inspect(other)}"
|
|
end
|
|
end
|
|
|
|
# Parse a tuple with name and fields and returns a
|
|
# list of second order tuples where the first element
|
|
# is the field and the second is its default value.
|
|
defp parse_record({ _name, fields }) do
|
|
cons = List.foldr fields, { nil, 0 }, fn f, acc ->
|
|
{ :cons, 0, parse_field(f), acc }
|
|
end
|
|
{ :value, list, _ } = :erl_eval.expr(cons, [])
|
|
list
|
|
end
|
|
|
|
defp parse_field({ :typed_record_field, record_field, _type }) do
|
|
parse_field(record_field)
|
|
end
|
|
|
|
defp parse_field({ :record_field, _, key }) do
|
|
{ :tuple, 0, [key, {:atom, 0, :nil}] }
|
|
end
|
|
|
|
defp parse_field({ :record_field, _, key, value }) do
|
|
{ :tuple, 0, [key, value] }
|
|
end
|
|
end
|
|
|
|
defmodule Record.Extensions do
|
|
@moduledoc false
|
|
|
|
# Main entry point. It defines both default functions
|
|
# via `default_for` and extensions via `extension_for`.
|
|
def functions_for(key, default, i) do
|
|
[ default_for(key, default, i),
|
|
extension_for(key, default, i) ]
|
|
end
|
|
|
|
# Skip the __exception__ for defexception.
|
|
def default_for(:__exception__, _default, _i) do
|
|
nil
|
|
end
|
|
|
|
# Define the default functions for each field.
|
|
def default_for(key, _default, i) do
|
|
bin_update = "update_" <> atom_to_binary(key)
|
|
update = binary_to_atom(bin_update)
|
|
|
|
quote do
|
|
def unquote(update).(function, record) do
|
|
current = :erlang.element(unquote(i + 1), record)
|
|
:erlang.setelement(unquote(i + 1), record, function.(current))
|
|
end
|
|
end
|
|
end
|
|
|
|
# Define extensions based on the default type.
|
|
def extension_for(key, default, i) when is_list(default) do
|
|
bin_key = atom_to_binary(key)
|
|
prepend = :"prepend_#{bin_key}"
|
|
merge = :"merge_#{bin_key}"
|
|
|
|
quote do
|
|
def unquote(prepend).(value, record) do
|
|
current = :erlang.element(unquote(i + 1), record)
|
|
:erlang.setelement(unquote(i + 1), record, value ++ current)
|
|
end
|
|
|
|
def unquote(merge).(value, record) do
|
|
current = :erlang.element(unquote(i + 1), record)
|
|
:erlang.setelement(unquote(i + 1), record, Keyword.merge(current, value))
|
|
end
|
|
end
|
|
end
|
|
|
|
def extension_for(key, default, i) when is_number(default) do
|
|
bin_key = atom_to_binary(key)
|
|
increment = :"increment_#{bin_key}"
|
|
|
|
quote do
|
|
def unquote(increment).(value // 1, record) do
|
|
current = :erlang.element(unquote(i + 1), record)
|
|
:erlang.setelement(unquote(i + 1), record, current + value)
|
|
end
|
|
end
|
|
end
|
|
|
|
def extension_for(key, default, i) when is_boolean(default) do
|
|
bin_key = atom_to_binary(key)
|
|
toggle = :"toggle_#{bin_key}"
|
|
|
|
quote do
|
|
def unquote(toggle).(record) do
|
|
current = :erlang.element(unquote(i + 1), record)
|
|
:erlang.setelement(unquote(i + 1), record, not current)
|
|
end
|
|
end
|
|
end
|
|
|
|
def extension_for(_, _, _), do: nil
|
|
end
|