Packages
estructura
0.1.0
1.14.0
1.13.0
1.12.0
1.11.0
1.10.2
1.10.1
1.10.0
1.9.0
1.8.3
1.8.2
1.8.1
1.8.0
1.7.2
1.7.1
1.7.0
1.6.1
1.6.0
1.5.0
1.4.2
1.4.1
1.4.0
1.3.3
1.3.2
1.3.1
1.3.0
1.2.12
1.2.11
1.2.10
1.2.9
1.2.8
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.1
1.1.0
1.0.0
0.6.0
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.0
0.1.0
Extensions for Elixir structures.
Current section
Files
Jump to
Current section
Files
lib/hooks.ex
defmodule Estructura.Hooks do
@moduledoc false
@spec access_ast(boolean(), [atom()]) :: Macro.t()
defp access_ast(false, _fields), do: []
defp access_ast(true, fields) when is_list(fields) do
opening =
quote generated: true, location: :keep do
@behaviour Access
end
clauses =
for key <- fields do
quote generated: true, location: :keep do
@impl Access
def fetch(%__MODULE__{unquote(key) => value}, unquote(key)), do: {:ok, value}
@impl Elixir.Access
def pop(%__MODULE__{unquote(key) => value} = data, unquote(key)),
do: {value, %{data | unquote(key) => nil}}
@impl Elixir.Access
def get_and_update(%__MODULE__{unquote(key) => value} = data, unquote(key), fun) do
case fun.(value) do
:pop -> pop(data, unquote(key))
{current_value, new_value} -> {current_value, %{data | unquote(key) => new_value}}
end
end
end
end
closing =
quote generated: true, location: :keep do
@impl Elixir.Access
def fetch(%__MODULE__{}, _key), do: :error
@impl Elixir.Access
def pop(%__MODULE__{} = data, _key), do: {nil, data}
@impl Elixir.Access
def get_and_update(%__MODULE__{}, key, _),
do: raise(KeyError, key: key, term: __MODULE__)
end
[opening | clauses] ++ [closing]
end
@spec enumerable_ast(boolean(), [atom()]) :: Macro.t()
defp enumerable_ast(false, _fields), do: []
defp enumerable_ast(true, fields) when is_list(fields) do
count = Enum.count(fields)
quote generated: true, location: :keep, bind_quoted: [fields: fields, count: count] do
module = __MODULE__
defimpl Enumerable, for: __MODULE__ do
def count(_), do: {:ok, unquote(count)}
for key <- fields do
def member?(%unquote(module){} = s, {unquote(key), value}),
do: {:ok, match?(%{unquote(key) => ^value}, s)}
end
def member?(%unquote(module){}, _), do: {:ok, false}
def slice(%unquote(module){} = s) do
size = unquote(count)
list = s |> Map.from_struct() |> :maps.to_list()
{:ok, size, &Enumerable.List.slice(list, &1, &2, size)}
end
def reduce(s, acc, fun) do
s
|> Map.from_struct()
|> :maps.to_list()
|> Enumerable.List.reduce(acc, fun)
end
end
end
end
@spec collectable_ast(false | atom()) :: Macro.t()
defp collectable_ast(false), do: []
defp collectable_ast(field) when is_atom(field) do
quote generated: true, location: :keep, bind_quoted: [field: field] do
module = __MODULE__
defimpl Collectable, for: __MODULE__ do
@dialyzer {:nowarn_function, into: 1}
def into(%unquote(module){unquote(field) => %MapSet{} = old_value} = s) do
fun = fn
list, {:cont, x} ->
[x | list]
list, :done ->
%unquote(module){s | unquote(field) => MapSet.union(old_value, MapSet.new(list))}
_, :halt ->
:ok
end
{[], fun}
end
def into(%unquote(module){unquote(field) => old_value} = s) when is_list(old_value) do
fun = fn
list, {:cont, x} ->
[x | list]
list, :done ->
%unquote(module){s | unquote(field) => old_value ++ list}
_, :halt ->
:ok
end
{[], fun}
end
def into(%unquote(module){unquote(field) => %{} = old_value} = s) do
fun = fn
list, {:cont, x} ->
[x | list]
list, :done ->
%unquote(module){s | unquote(field) => Map.merge(old_value, Map.new(list))}
_, :halt ->
:ok
end
{[], fun}
end
def into(%unquote(module){unquote(field) => old_value} = s) when is_binary(old_value) do
fun = fn
acc, {:cont, x} when is_binary(x) and is_list(acc) ->
[acc | x]
acc, {:cont, x} when is_bitstring(x) and is_bitstring(acc) ->
<<acc::bitstring, x::bitstring>>
acc, {:cont, x} when is_bitstring(x) ->
<<IO.iodata_to_binary(acc)::bitstring, x::bitstring>>
acc, :done when is_bitstring(acc) ->
%unquote(module){s | unquote(field) => acc}
acc, :done ->
%unquote(module){s | unquote(field) => IO.iodata_to_binary(acc)}
__acc, :halt ->
:ok
end
{[old_value], fun}
end
def into(%unquote(module){unquote(field) => old_value} = s)
when is_bitstring(old_value) do
fun = fn
acc, {:cont, x} when is_bitstring(x) ->
<<acc::bitstring, x::bitstring>>
acc, :done ->
%unquote(module){s | unquote(field) => acc}
_acc, :halt ->
:ok
end
{old_value, fun}
end
end
end
end
@spec generator_ast(false | keyword()) :: Macro.t()
defp generator_ast(false), do: []
if match?({:module, StreamData}, Code.ensure_compiled(StreamData)) do
defp generator_ast([{_, _} | _] = types) do
types = Macro.escape(types)
quote generated: true, location: :keep, bind_quoted: [types: types] do
module = __MODULE__
@__generator__ %{types: types, fields: Keyword.keys(types)}
defp fix_gen(many) when is_list(many), do: Enum.map(many, &fix_gen/1)
defp fix_gen({key, {mod, fun, args} = value})
when is_atom(mod) and is_atom(fun) and is_list(args),
do: {key, fix_gen(value)}
defp fix_gen({key, {mod, fun}}) when is_atom(mod) and is_atom(fun),
do: fix_gen({key, {mod, fun, []}})
defp fix_gen({mod, fun, args}) when is_atom(mod) and is_atom(fun) and is_list(args) do
{{:., [], [mod, fun]}, [], fix_gen(args)}
end
defp fix_gen({mod, fun}) when is_atom(mod) and is_atom(fun), do: fix_gen({mod, fun, []})
defp fix_gen(value), do: value
# @dialyzer {:nowarn_function, generation_leaf: 1}
defp generation_leaf(args),
do: {{:., [], [StreamData, :constant]}, [], [{:{}, [], args}]}
defp generation_clause({arg, gen}, acc) do
{{:., [], [StreamData, :bind]}, [], [gen, {:fn, [], [{:->, [], [[arg], acc]}]}]}
end
defp generation_bound do
args =
Enum.map(@__generator__.types, fn {arg, gen} ->
{Macro.var(arg, nil), fix_gen(gen)}
end)
init_args = args |> Enum.map(&elem(&1, 0))
Enum.reduce(args, generation_leaf(init_args), &generation_clause/2)
end
defmacrop do_generation, do: generation_bound()
@doc false
@spec __generator__() :: StreamData.t(%__MODULE__{})
def __generator__, do: __generator__(%__MODULE__{})
@spec __generator__(%__MODULE__{}) :: StreamData.t()
def __generator__(%__MODULE__{} = this) do
do_generation()
|> StreamData.map(&Tuple.to_list/1)
|> StreamData.map(&Enum.zip(@__generator__.fields, &1))
|> StreamData.map(&struct(this, &1))
end
defoverridable __generator__: 0, __generator__: 1
end
end
end
##############################################################################
@spec fields(module()) :: [atom()]
defp fields(module) do
module
|> Module.get_attribute(:__struct__, %{})
|> Map.delete(:__struct__)
|> Map.keys()
end
def __after_compile__(_env, _bytecode) do
# IO.inspect(env.module.__struct__, label: "AFTER")
end
defmacro __before_compile__(env) do
module = env.module
fields = fields(module)
config = Module.get_attribute(module, :__estructura__)
access_ast = access_ast(Map.get(config, :access, false), fields)
field = Map.get(config, :collectable, false)
if field && field not in fields, do: raise(KeyError, key: field, term: __MODULE__)
collectable_ast = collectable_ast(field)
# [MAYBE] fields |> Enum.zip(Stream.cycle([StreamData.term()])) |> Keyword.merge(types),
types =
with {:module, _} <- Code.ensure_compiled(StreamData),
types when is_list(types) <- Map.get(config, :generator, false),
true <- Keyword.keyword?(types),
do: types,
else: (_ -> false)
enumerable_ast = enumerable_ast(Map.get(config, :enumerable, false), fields)
generator_ast = generator_ast(types)
[access_ast, enumerable_ast, collectable_ast, generator_ast]
|> Enum.map(&List.wrap/1)
|> Enum.reduce(&Kernel.++/2)
end
end