Packages
surface
0.7.1
0.12.3
0.12.2
0.12.1
0.12.0
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
0.1.0-alpha.2
0.1.0-alpha.1
0.1.0-alpha.0
A component based library for Phoenix LiveView
Current section
Files
Jump to
Current section
Files
lib/surface/catalogue/data.ex
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