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. @moduledoc """ Extract data from nested Elixir data structure using JSONPath-like path expressions. See [this page](readme.html) for syntax. @typedoc \""" Elixpath, already compiled by `Elixpath.Parser.parse/2` or `sigil_p/2`. """ @type t :: %__MODULE__{path: [PathComponent.t()]} defstruct path: [] require Elixpath.PathComponent, as: PathComponent require Elixpath.Tag, as: Tag @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: [Elixpath.PathComponent.child(key)]}, _gots, opts) do IO.inspect(key) Elixpath.Access.query(data, key, opts) end defp do_query( data, %Elixpath{path: [Elixpath.PathComponent.child(key) | rest]} = path, _gots, opts ) do IO.inspect(key) 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 @spec update(data :: term, t | String.t(), any) :: term def update(data, path_or_str, opts \\ "") def update(data, str, opts) when is_binary(str) do with {:ok, compiled_path} <- Elixpath.Parser.parse(str, []) do update(data, compiled_path, opts) end end def update(data, %Elixpath{} = path, opts) do do_update(data, path, opts) end @spec do_update(term, t, any) :: term defp do_update(data, %Elixpath{path: []}, _opts), do: data defp do_update(data, %Elixpath{path: [PathComponent.child(key)]}, update_val) do Elixpath.Update.update(data, key, update_val) end defp do_update(data, %Elixpath{path: [PathComponent.child(key) | rest]}, update_val) when is_map(data) do case Elixpath.Access.query(data, key, []) do {:ok, [sub_data]} -> new_data = do_update(sub_data, %Elixpath{path: rest}, update_val) updated_data = Map.put(data, key, new_data) updated_data _ -> data end end defp do_update( data, %Elixpath{path: [PathComponent.child(Tag.wildcard()) | rest]}, update_val ) when is_list(data) do new_data = data |> Enum.map(fn v -> new_val = do_update(v, %Elixpath{path: rest}, update_val) new_val end) new_data end defp do_update(data, %Elixpath{path: [PathComponent.child(key) | rest]}, update_val) when is_list(data) do target_data = Enum.at(data, key) updated_target_data = do_update(target_data, %Elixpath{path: rest}, update_val) new_data = List.update_at(data, key, fn _v -> updated_target_data end) new_data 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