Skip to content

JSONPath-like operations for Elixir's native data structure

License

Notifications You must be signed in to change notification settings

mtannaan/elixpath

Repository files navigation

Elixpath

.github/workflows/ci.yml codecov MIT License Hex.pm hexdocs.pm Hex.pm

Extract data from Elixir's native data structure using JSONPath-like path expressions.

Searching for XPath Tools?

If you are planning to manipulate XML documents directly, other packages like sweet_xml can be better choices.

Elixpath Expression

Elixpath's path expression is based on JSONPath, but mainly with following differences:

  • Following Elixir's native expressions are supported:
    • String, e.g. ..string."double-quoted string"
    • Atom, e.g. .:atom.:"quoted atom"
    • Charlist, e.g. .'single-quoted'
    • Integer, e.g. [1][-1]
  • Several JSONPath features like following are not supported:
    • "Current object" syntax element: @
    • Union subscript operator: [xxx,yyy]
    • Array slice operator: [start:end:stop]
    • Filter and script expression using ()

Path Syntax

An Elixpath is represented by a sequence of following path components.

  • $ - root object. Optional. When present, this component has to be at the beginning of the path.
  • .(key expression) or [(key expression)] - child objects that matches the given key.
  • ..(key expression) - descendant objects that matches the given key.

(key expression) above can be either of:

  • integer - used to specify index in lists. Value can be negative, e.g. -1 represents the last element.
  • atom - starts with colon and can be quoted, e.g. :atom, :"quoted_atom". When prefer_keys: :atom option is given, preceding colon can be omitted.
  • string - double-quoted. Double quotation can be omitted unless prefer_keys: :atom option is given.
  • charlist - single-quoted.
  • wildcard - *. Represents all the children.

Examples

# string
iex> Elixpath.query(%{:a => 1, "b" => 2}, ~S/."b"/)
{:ok, [2]}

# you can use Elixpath.get! if you want only a single match
iex> Elixpath.get!(%{:a => 1, "b" => 2}, ".*")
1

# unquoted string
iex> Elixpath.query(%{:a => 1, "b" => 2}, ".b")
{:ok, [2]}

# no match
iex> Elixpath.query(%{:a => 1, "b" => 2}, ".nonsense")
{:ok, []}

# no match w/ get!
iex> Elixpath.get!(%{:a => 1, "b" => 2}, ".nonsense", _default = :some_default_value)
:some_default_value

# atom
iex> Elixpath.query(%{:a => 1, "b" => 2}, ".:a")
{:ok, [1]}

# integer
iex> Elixpath.query(%{:a => [%{b: 2}, %{c: 3}]}, ".:a[-1]")
{:ok, [%{c: 3}]}

# descendant
iex> Elixpath.query(%{:a => [%{b: 2}, %{c: 3}]}, "..:c")
{:ok, [3]}

# wildcard
iex> Elixpath.query(%{:a => [%{b: 2}, %{c: 3}]}, ".*.*.*")
{:ok, [2, 3]}

# enable sigil_p/2, which parses Elixpath at compile time.
iex> import Elixpath
iex> Elixpath.query(%{:a => [%{b: 2}, %{c: 3}]}, ~p".:a.1.:c")
{:ok, [3]}

# path syntax error for normal string is detected at runtime.
iex> Elixpath.query(%{:a => [%{b: 2}, %{c: 3}]}, ".:atom:syntax:error")
{:error, "expected member_expression while processing path"}

# while sigil_p raises a compilation error:
# iex> Elixpath.query(%{:a => [%{b: 2}, %{c: 3}]}, ~p".:atom:syntax:error")
# == Compilation error in file test/elixpath_test.exs ==
# ** (Elixpath.Parser.ParseError) ...