Packages

Telepath is, in essence, xPath/JsonPath for elixir structs.

Current section

Files

Jump to
telepath lib telepath.ex
Raw

lib/telepath.ex

defmodule Telepath do
@moduledoc """
Provide an easy way to access elixir's data struct with a path.
Inspired by JsonPath & xPath (for json and xml), Telepath allows you to reach
the data that you want, simply by specifying a path.
> The path can be created using the sigil: ~t (see `Telepath.sigil_t/2`).
"""
@type path :: [String.t() | atom | number]
@regex ~r/^\w+|[0-9]+|\*/
@doc """
Transform the struct path to an array that defines how to
access the data.
> Use `~t` instead of `&Telepath.sigil_t/2`.
## Modifiers
The modifiers available when creating a Telepath are:
- atom (a) - enable atom keys for path exploration.
## Special characters
- `*`: will be transformed to `:*` (see `*` in `Telepath.get/3`)
## Examples
iex> Telepath.sigil_t("node")
["node"]
iex> Telepath.sigil_t("node.attr1")
["node", "attr1"]
iex> Telepath.sigil_t("node[0]")
["node", 0]
iex> Telepath.sigil_t("node[0].attr1")
["node", 0, "attr1"]
iex> Telepath.sigil_t("node.0.attr1")
["node", "0", "attr1"]
iex> Telepath.sigil_t("node.*.attr1")
["node", :*, "attr1"]
With the sigil `~t` it will be as simple as:
```elixir
~t/data/
# ["data"]
~t/data/a
# [:data]
```
"""
@spec sigil_t(String.t(), List.t()) :: __MODULE__.path()
def sigil_t(string, opts \\ []) do
String.split(string, ".")
|> Enum.map(fn p ->
[node | array_pos] =
@regex
|> Regex.scan(p)
|> List.flatten()
array_pos = Enum.map(array_pos, &String.to_integer/1)
cond do
node == "*" ->
[:* | array_pos]
Enum.member?(opts, ?a) ->
[String.to_atom(node) | array_pos]
true ->
[node | array_pos]
end
end)
|> List.flatten()
end
@doc """
Obtains a data at a given path.
```elixir
Telepath.get(%{hello: "world"}, ~t/hello/a)
# "world"
Telepath.get(%{foo: [%{bar: "bar1"}, %{bar: "bar2"}]}, ~t/foo.bar/a)
# ["bar1", "bar2"]
# works also with string key
Telepath.get(%{"foo" => [%{"bar" => "bar1"}, %{"bar" => "bar2"}]}, ~t/foo.bar/)
# ["bar1", "bar2"]
Telepath.get(%{foo: [%{bar: "bar1"}, %{bar: "bar2"}]}, ~t/foo/a)
# [%{bar: "bar1"}, %{bar: "bar2"}]
Telepath.get(%{foo: [%{bar: "bar1"}, %{bar: "bar2"}]}, ~t/*/a)
# [%{bar: "bar1"}, %{bar: "bar2"}]
```
If you want to map every case in the path, you can use `:*`.
e.g.
```elixir
Telepath.get(
%{data: %{key1: "value1", key2: "value2"}},
~t/data.*/a
)
# ["value1", "value2"]
```
> See `sigil_t/2` for more informations on path.
### opts
- `flatten` Return a flattened list (works only if the result of telepath is a list) (default `false`).
"""
def get(data, path, opts \\ []) do
result = do_get(data, path)
if is_list(result) && Keyword.get(opts, :flatten) do
List.flatten(result)
else
result
end
end
@spec do_get(data :: any, path :: __MODULE__.path()) :: any
defp do_get(data, _path = []), do: data
defp do_get(data, _) when not is_map(data) and not is_list(data) do
nil
end
defp do_get(data, [:* | path]) when is_map(data) do
Enum.map(data, fn {_k, v} -> get(v, path) end)
end
defp do_get(data, [:* | path]) when is_list(data) do
if Keyword.keyword?(data) do
data
|> Keyword.values()
|> get(path)
else
Enum.map(data, &get(&1, path))
end
end
defp do_get(data, [x | path]) when is_list(data) and is_integer(x) do
data |> Enum.at(x) |> get(path)
end
defp do_get(data, [x | path]) when is_list(data) do
if Keyword.keyword?(data) do
data
|> Keyword.get_values(x)
|> get(path)
else
Enum.map(data, &get(&1, [x | path]))
end
end
defp do_get(data, [x | expresion]) when is_map(data) do
data
|> Map.get(x)
|> get(expresion)
end
end