README.md

# Elixpath
[![Build Status](https://travis-ci.com/mtannaan/elixpath.svg?branch=master)](https://travis-ci.com/mtannaan/elixpath) [![codecov](https://codecov.io/gh/mtannaan/elixpath/branch/master/graph/badge.svg)](https://codecov.io/gh/mtannaan/elixpath) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

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](https://hex.pm/packages/sweet_xml) can be better choices.

## Elixpath Expression
Elixpath's path expression is based on [JSONPath](https://goessner.net/articles/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
```elixir
# 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) ...
```