118 lines
3.4 KiB
Elixir
118 lines
3.4 KiB
Elixir
defmodule Record.Extractor do
|
|
@moduledoc false
|
|
|
|
def extract(name, opts) do
|
|
extract_record(name, from_or_from_lib_file(opts))
|
|
end
|
|
|
|
def extract_all(opts) do
|
|
extract_all_records(from_or_from_lib_file(opts))
|
|
end
|
|
|
|
defp from_or_from_lib_file(opts) do
|
|
cond do
|
|
file = opts[:from] ->
|
|
{from_file(file), Keyword.delete(opts, :from)}
|
|
|
|
file = opts[:from_lib] ->
|
|
{from_lib_file(file), Keyword.delete(opts, :from_lib)}
|
|
|
|
true ->
|
|
raise ArgumentError, "expected :from or :from_lib to be given as option"
|
|
end
|
|
end
|
|
|
|
# Find file using the same lookup as the *include* attribute from Erlang modules.
|
|
defp from_file(file) do
|
|
file = String.to_charlist(file)
|
|
|
|
case :code.where_is_file(file) do
|
|
:non_existing -> file
|
|
realfile -> realfile
|
|
end
|
|
end
|
|
|
|
# Find file using the same lookup as the *include_lib* attribute from Erlang modules.
|
|
defp from_lib_file(file) do
|
|
[app | path] = :filename.split(String.to_charlist(file))
|
|
|
|
case :code.lib_dir(List.to_atom(app)) do
|
|
{:error, _} ->
|
|
raise ArgumentError, "lib file #{file} could not be found"
|
|
|
|
libpath ->
|
|
:filename.join([libpath | path])
|
|
end
|
|
end
|
|
|
|
# Retrieve the record with the given name from the given file
|
|
defp extract_record(name, {file, opts}) do
|
|
form = read_file(file, opts)
|
|
records = extract_records(form)
|
|
|
|
if record = List.keyfind(records, name, 0) do
|
|
parse_record(record, form)
|
|
else
|
|
raise ArgumentError,
|
|
"no record #{name} found at #{file}. Or the record does not exist or " <>
|
|
"its entry is malformed or depends on other include files"
|
|
end
|
|
end
|
|
|
|
# Retrieve all records from the given file
|
|
defp extract_all_records({file, opts}) do
|
|
form = read_file(file, opts)
|
|
records = extract_records(form)
|
|
for rec = {name, _fields} <- records, do: {name, parse_record(rec, form)}
|
|
end
|
|
|
|
# Parse the given file and extract all existent records.
|
|
defp extract_records(form) do
|
|
for {:attribute, _, :record, record} <- form, do: record
|
|
end
|
|
|
|
# Read a file and return its abstract syntax form that also
|
|
# includes record but with macros and other attributes expanded,
|
|
# such as "-include(...)" and "-include_lib(...)". This is done
|
|
# by using Erlang's epp.
|
|
defp read_file(file, opts) do
|
|
case :epp.parse_file(file, opts) do
|
|
{:ok, form} ->
|
|
form
|
|
|
|
other ->
|
|
raise "error parsing file #{file}, got: #{inspect(other)}"
|
|
end
|
|
end
|
|
|
|
# Parse a tuple with name and fields and returns a
|
|
# list of tuples where the first element is the field
|
|
# and the second is its default value.
|
|
defp parse_record({_name, fields}, form) do
|
|
cons = List.foldr(fields, {nil, 0}, fn f, acc -> {:cons, 0, parse_field(f), acc} end)
|
|
eval_record(cons, form)
|
|
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, :undefined}]}
|
|
end
|
|
|
|
defp parse_field({:record_field, _, key, value}) do
|
|
{:tuple, 0, [key, value]}
|
|
end
|
|
|
|
defp eval_record(cons, form) do
|
|
form = form ++ [{:function, 0, :hello, 0, [{:clause, 0, [], [], [cons]}]}]
|
|
|
|
{:function, 0, :hello, 0, [{:clause, 0, [], [], [record_ast]}]} =
|
|
:erl_expand_records.module(form, []) |> List.last()
|
|
|
|
{:value, record, _} = :erl_eval.expr(record_ast, [])
|
|
record
|
|
end
|
|
end
|