defmodule Surface.Catalogue.Data do
@moduledoc """
Experimental module that provides conveniences for manipulating data
in Examples and Playgrounds.
Provide wrappers around built-in functions like `get_in/2` and `update_in/3`
using a shorter syntax for accessors.
## Accessor Mapping
* `[_]`: `Access.all/0`
* `[fun]`: `Access.filter(fun)`
* `[index]`: `Access.at(index)` (Shorthand for `[index: i]`)
* `[index: i]`: `Access.at(i)`
* `[key: k]`: `Access.key(k)`
* `[first..last]`: `Surface.Catalogue.Data.slice(first..last)`
## Example
Data.get(props.lists[_].cards[& &1.id == "Card_1"].tags[-1].name)
The code above will be translated to:
get_in(props, [:lists, Access.all, :cards, Access.filter(& &1.id == "Card_1"), :tags, Access.at(-1), :name])
"""
@doc """
Generates a short ramdom id.
"""
def random_id(size \\ 6) do
:crypto.strong_rand_bytes(size)
|> Base.encode32(case: :lower)
|> binary_part(0, size)
end
@doc """
Gets an existing value from the given nested structure.
Raises an error if none or more than one value is found.
"""
defmacro get!(path) do
{subject, selector} = split_path(path)
quote do
unquote(__MODULE__).__get__!(unquote(subject), unquote(selector))
end
end
@doc """
Gets a value from the given nested structure.
A wrapper around `get_in/2`
"""
defmacro get(path) do
{subject, selector} = split_path(path)
quote do
get_in(unquote(subject), unquote(selector))
end
end
@doc """
Gets a value and updates a given nested structure.
A wrapper around `get_and_update_in/3`
"""
defmacro get_and_update(path, fun) do
{subject, selector} = split_path(path)
quote do
get_and_update_in(unquote(subject), unquote(selector), unquote(fun))
end
end
@doc """
Pops a item from the given nested structure.
A wrapper around `pop_in/2`
"""
defmacro pop(path) do
{subject, selector} = split_path(path)
quote do
pop_in(unquote(subject), unquote(selector))
end
end
@doc """
Updates an item in the given nested structure.
A wrapper around `update_in/2`
"""
defmacro update(path, fun) do
{subject, selector} = split_path(path)
quote do
update_in(unquote(subject), unquote(selector), unquote(fun))
end
end
@doc """
Deletes an item from the given nested structure.
"""
defmacro delete(path) do
{subject, selector} = split_path(path)
quote do
unquote(__MODULE__).__delete__(unquote(subject), unquote(selector))
end
end
@doc """
Inserts an item into a list in the given nested structure.
"""
defmacro insert_at(path, pos, value) do
{subject, selector} = split_path(path)
quote do
unquote(__MODULE__).__insert_at__(
unquote(subject),
unquote(selector),
unquote(pos),
unquote(value)
)
end
end
@doc """
Appends an item to a list in the given nested structure.
"""
defmacro append(path, value) do
{subject, selector} = split_path(path)
quote do
unquote(__MODULE__).__insert_at__(unquote(subject), unquote(selector), -1, unquote(value))
end
end
@doc """
Prepends an item to a list in the given nested structure.
"""
defmacro prepend(path, value) do
{subject, selector} = split_path(path)
quote do
unquote(__MODULE__).__insert_at__(unquote(subject), unquote(selector), 0, unquote(value))
end
end
@doc false
def __get__!(subject, selector) do
case get_in(subject, selector) |> List.flatten() do
[item] ->
item
[] ->
raise "no value found"
[_ | _] ->
raise "more than one value found"
end
end
@doc false
def __insert_at__(subject, selector, pos, value) do
update_in(subject, selector, fn list ->
List.insert_at(list, pos, value)
end)
end
@doc false
def __delete__(subject, selector) do
{_, list} = pop_in(subject, selector)
list
end
@doc false
def access_fun(value) when is_function(value) do
Access.filter(value)
end
def access_fun(value) when is_integer(value) do
Access.at(value)
end
def access_fun(from..to = range) when is_integer(from) and is_integer(to) do
slice(range)
end
def access_fun(value) do
Access.key(value)
end
defp quoted_access_fun({:_, _, _}) do
quote do
Access.all()
end
end
defp quoted_access_fun(key: value) do
quote do
Access.key(unquote(value))
end
end
defp quoted_access_fun(index: value) do
quote do
Access.at(unquote(value))
end
end
defp quoted_access_fun(value) do
quote do
unquote(__MODULE__).access_fun(unquote(value))
end
end
def slice(range) do
fn op, data, next -> slice(op, data, range, next) end
end
defp slice(:get, data, range, next) when is_list(data) do
data |> Enum.slice(range) |> Enum.map(next)
end
defp slice(:get_and_update, data, range, next) when is_list(data) do
get_and_update_slice(data, range, next, [], [], -1)
end
defp slice(_op, data, _range, _next) do
raise "slice expected a list, got: #{inspect(data)}"
end
defp normalize_range_bound(value, list_length) do
if value < 0 do
value + list_length
else
value
end
end
defp get_and_update_slice([], _range, _next, updates, gets, _index) do
{:lists.reverse(gets), :lists.reverse(updates)}
end
defp get_and_update_slice(list, from..to, next, updates, gets, -1) do
list_length = length(list)
from = normalize_range_bound(from, list_length)
to = normalize_range_bound(to, list_length)
get_and_update_slice(list, from..to, next, updates, gets, 0)
end
defp get_and_update_slice([head | rest], from..to = range, next, updates, gets, index) do
new_index = index + 1
if index >= from and index <= to do
case next.(head) do
{get, update} ->
get_and_update_slice(rest, range, next, [update | updates], [get | gets], new_index)
:pop ->
get_and_update_slice(rest, range, next, updates, [head | gets], new_index)
end
else
get_and_update_slice(rest, range, next, [head | updates], gets, new_index)
end
end
defp split_path(path) do
{[subject | rest], _} = unnest(path, [], true, "test")
{subject, convert_selector(rest)}
end
defp convert_selector(list) do
Enum.map(list, fn
{:map, key} ->
quote do
Access.key!(unquote(key))
end
{:access, expr} ->
quoted_access_fun(expr)
end)
end
def unnest(path) do
unnest(path, [], true, "test")
end
defp unnest({{:., _, [Access, :get]}, _, [expr, key]}, acc, _all_map?, kind) do
unnest(expr, [{:access, key} | acc], false, kind)
end
defp unnest({{:., _, [expr, key]}, _, []}, acc, all_map?, kind)
when is_tuple(expr) and :erlang.element(1, expr) != :__aliases__ and
:erlang.element(1, expr) != :__MODULE__ do
unnest(expr, [{:map, key} | acc], all_map?, kind)
end
defp unnest(other, [], _all_map?, kind) do
raise ArgumentError,
"expected expression given to #{kind} to access at least one element, " <>
"got: #{Macro.to_string(other)}"
end
defp unnest(other, acc, all_map?, kind) do
case proper_start?(other) do
true ->
{[other | acc], all_map?}
false ->
raise ArgumentError,
"expression given to #{kind} must start with a variable, local or remote call " <>
"and be followed by an element access, got: #{Macro.to_string(other)}"
end
end
defp proper_start?({{:., _, [expr, _]}, _, _args})
when is_atom(expr)
when :erlang.element(1, expr) == :__aliases__
when :erlang.element(1, expr) == :__MODULE__,
do: true
defp proper_start?({atom, _, _args})
when is_atom(atom),
do: true
defp proper_start?(other), do: not is_tuple(other)
end