defmodule Elixpath do
# Import some example from README.md to run doctests.
# Make sure to touch (i.e. update timestamp of) this file
# when editing examples in README.md.
readme = File.read!(__DIR__ |> Path.expand() |> Path.dirname() |> Path.join("README.md"))
[examples] = Regex.run(~r/##\s*Examples.+/s, readme)
@moduledoc """
Extract data from nested Elixir data structure using JSONPath-like path expressions.
See [this page](readme.html) for syntax.
""" <> examples
require Elixpath.PathComponent, as: PathComponent
require Elixpath.Tag, as: Tag
@typedoc """
Elixpath, already compiled by `Elixpath.Parser.parse/2` or `sigil_p/2`.
"""
@type t :: %__MODULE__{path: [PathComponent.t()]}
defstruct path: []
@doc """
Compiles string to internal Elixpath representation.
Warning: Do not specify `unsafe_atom` modifier (`u`) for untrusted input.
See `String.to_atom/1`, which this function uses to create new atom, for details.
## Modifiers
* `unsafe_atom` (u) - passes `unsafe_atom: true` option to `Elixpath.Parser.parse/2`.
* `atom_keys_preferred` (a) - passes `prefer_keys: :atom` option to `Elixpath.Parser.parse/2`.
## Examples
iex> import Elixpath, only: [sigil_p: 2]
iex> ~p/.string..:b[1]/
#Elixpath<[elixpath_child: "string", elixpath_descendant: :b, elixpath_child: 1]>
iex> ~p/.atom..:b[1]/a
#Elixpath<[elixpath_child: :atom, elixpath_descendant: :b, elixpath_child: 1]>
"""
defmacro sigil_p({:<<>>, _meta, [str]}, modifiers) do
opts = [
unsafe_atom: ?u in modifiers,
prefer_keys: if(?a in modifiers, do: :atom, else: :string)
]
path = Elixpath.Parser.parse!(str, opts) |> Macro.escape()
quote do
unquote(path)
end
end
@doc """
Query data from nested data structure.
Returns list of matches, wrapped by `:ok`.
When no match, `{:ok, []}` is returned.
## Options
For path parsing options, see `Elixpath.Parser.parse/2`.
"""
@spec query(data :: term, t | String.t(), [Elixpath.Parser.option()]) ::
{:ok, [term]} | {:error, reason :: term}
def query(data, path_or_str, opts \\ [])
def query(data, str, opts) when is_binary(str) do
with {:ok, compiled_path} <- Elixpath.Parser.parse(str, opts) do
query(data, compiled_path, opts)
end
end
def query(data, %Elixpath{} = path, opts) do
do_query(data, path, _gots = [], opts)
end
@spec do_query(term, t, list, Keyword.t()) :: {:ok, [term]} | {:error, reason :: term}
defp do_query(_data, %Elixpath{path: []}, _gots, _opts), do: {:ok, []}
defp do_query(data, %Elixpath{path: [PathComponent.child(key)]}, _gots, opts) do
Elixpath.Access.query(data, key, opts)
end
defp do_query(data, %Elixpath{path: [PathComponent.child(key) | rest]} = path, _gots, opts) do
with {:ok, children} <- Elixpath.Access.query(data, key, opts) do
Enum.reduce_while(children, {:ok, []}, fn child, {:ok, gots_acc} ->
case do_query(child, %{path | path: rest}, gots_acc, opts) do
{:ok, fetched} -> {:cont, {:ok, gots_acc ++ fetched}}
error -> {:halt, error}
end
end)
end
end
defp do_query(data, %Elixpath{path: [PathComponent.descendant(key) | rest]} = path, gots, opts) do
with direct_path <- %{path | path: [PathComponent.child(key) | rest]},
{:ok, direct_children} <- do_query(data, direct_path, gots, opts),
indirect_path <- %{
path
| path: [
PathComponent.child(Tag.wildcard()),
PathComponent.descendant(key) | rest
]
},
{:ok, indirect_children} <- do_query(data, indirect_path, gots, opts) do
{:ok, direct_children ++ indirect_children}
end
end
@doc """
Query data from nested data structure.
Same as `query/3`, except that `query!/3`raises on error.
Returns `[]` when no match.
"""
@spec query!(data :: term, t | String.t(), [Elixpath.Parser.option()]) :: [term] | no_return
def query!(data, path, opts \\ []) do
case query(data, Elixpath.Parser.parse!(path, opts), opts) do
{:ok, got} -> got
end
end
@doc """
Get *single* data from nested data structure.
Returns `default` when no match.
Raises on error.
"""
@spec get!(data :: term, t | String.t(), default, [Elixpath.Parser.option()]) ::
term | default | no_return
when default: term
def get!(data, path_or_str, default \\ nil, opts \\ [])
def get!(data, str, default, opts) when is_binary(str) do
get!(data, Elixpath.Parser.parse!(str, opts), default, opts)
end
def get!(data, %Elixpath{} = path, default, opts) do
case query!(data, path, opts) do
[] -> default
[head | _rest] -> head
end
end
@doc ~S"""
Converts Elixpath to string.
Also available via `Kernel.to_string/1`.
This function is named `stringify/1` to avoid name collision
with `Kernel.to_string/1` when the entire module is imported.
## Examples
iex> import Elixpath, only: [sigil_p: 2]
iex> path = ~p/.1.child..:decendant/u
#Elixpath<[elixpath_child: 1, elixpath_child: "child", elixpath_descendant: :decendant]>
iex> path |> to_string()
"[1].\"child\"..:decendant"
iex> "interpolation: #{~p/..1[*]..*/}"
"interpolation: ..[1].*..*"
"""
@spec stringify(t) :: String.t()
def stringify(path) do
Enum.map_join(path.path, fn
PathComponent.child(Tag.wildcard()) -> ".*"
PathComponent.descendant(Tag.wildcard()) -> "..*"
PathComponent.child(int) when is_integer(int) -> "[#{inspect(int)}]"
PathComponent.descendant(int) when is_integer(int) -> "..[#{inspect(int)}]"
PathComponent.child(x) -> ".#{inspect(x)}"
PathComponent.descendant(x) -> "..#{inspect(x)}"
end)
end
end
defimpl Inspect, for: Elixpath do
@spec inspect(Elixpath.t(), Inspect.Opts.t()) :: Inspect.Algebra.t()
def inspect(path, opts) do
Inspect.Algebra.concat(["#Elixpath<", Inspect.Algebra.to_doc(path.path, opts), ">"])
end
end
defimpl String.Chars, for: Elixpath do
@spec to_string(Elixpath.t()) :: binary
def to_string(path), do: Elixpath.stringify(path)
end
defimpl List.Chars, for: Elixpath do
@spec to_charlist(Elixpath.t()) :: charlist
def to_charlist(path), do: Elixpath.stringify(path) |> String.to_charlist()
end