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