README.md

[![Actions Status](https://github.com/cleidiano/warpath/workflows/build/badge.svg?branch=master)](https://github.com/cleidiano/warpath/actions)
[![Warpath version](https://img.shields.io/hexpm/v/warpath.svg)](https://hex.pm/packages/warpath)

# Warpath
<!-- MDOC !-->
A implementation of Jsonpath expression proposal by [Stefan Goessner](https://goessner.net/articles/JsonPath/) for Elixir.


The following apis are available: `Warpath.query/3`, `Warpath.delete/2`, `Warpath.update/3`.

## Operators
  | Operator                  | Description                                                        |
  | :------------------------ | :----------------------------------------------------------------- |
  | `$`                       | The root element to query. This starts all path expressions.       |
  | `@`                       | The current node being processed by a filter predicate.            |
  | `*`                       | Wildcard. All objects/elements regardless their names.             |
  | `..`                      | Deep scan, recursive descent.                                      |
  | `.name`                   | Dot-notated child, it support string or atom as keys.              |
  | `['name']`,`["name"]`     | Bracket-notated child, it support string or atom as keys.          |
  | `[int (,int>)]`           | Array index or indexes                                             |
  | `[start:end:step]`        | Array slice operator. Start index **inclusive**, end index **exclusive**. |
  | `[?(expression)]`         | Filter expression. Expression must evaluate to a boolean value.    |

## Filter operators
  All filter operator supported by Warpath have the same behavior of Elixir lang,
  it means that it's possible to compare different data types, check the [Elixir getting started](https://elixir-lang.org/getting-started/basic-operators.html)
  page for more information about cross comparision on data types.

  Filter are expression that must be result on a boolean value, Warpath will use then to retain data when filter a data structure;
  a filter expression have it syntax like this `[?( @.category == 'fiction' )]`.

  | Operator                 | Description                                                         |
  | :----------------------- | :------------------------------------------------------------------ |
  | ==                       | left is equal to right                                              |
  | ===                      | left is equal to right in strict mode                               |
  | !=                       | left is not equal to right                                          |
  | !==                      | left is not equal to right in strict mode                           |
  | <                        | left is less than right                                             |
  | <=                       | left is less or equal to right                                      |
  | >                        | left is greater than right                                          |
  | >=                       | left is greater than or equal to right                              |
  | in                       | left exists in right `[?(@.price in [10, 20, 30])]`                 |
  | and,&&                   | logical and operator `[?(@.price > 50 and @.price < 100)]`          |
  | or,\|\|                  | logical or operator `[?(@.category == 'fiction' or @.price < 100)]` |
  | not                      | logical not operator `[?(not @.category == 'fiction')]`             |

### Functions allowed in filter expression
  | Function            | Description                   |
  | :------------------ | :--------------------------- |
  | is_atom/1           | check if the given expression argument is evaluate to atom       |
  | is_binary/1         | check if the given expression argument is evaluate to binary     |
  | is_boolean/1        | check if the given expression argument is evaluate to boolean    |
  | is_float/1          | check if the given expression argument is evaluate to float      |
  | is_integer/1        | check if the given expression argument is evaluate to integer    |
  | is_list/1           | check if the given expression argument is evaluate to list       |
  | is_map/1            | check if the given expression argument is evaluate to map        |
  | is_nil/1            | check if the given expression argument is evaluate to nil        |
  | is_number/1         | check if the given expression argument is evaluate to number     |
  | is_tuple/1          | check if the given expression argument is evaluate to tuple      |

## Examples
### All children
```elixir
    #wildcard using bracket-notation
    iex> document = %{"elements" => [:a, :b, :c]}
    ...> Warpath.query(document, "$.elements[*]")
    {:ok, [:a, :b, :c]}

    #wildcard using dot-notation
    iex> document = %{"integers" => [100, 200, 300]}
    ...> Warpath.query(document, "$.integers.*")
    {:ok, [100, 200, 300]}
```

### Children lookup by name
```elixir
    #Simple string
    iex> Warpath.query(%{"category" => "fiction", "price" => 12.99}, "$.category")
    {:ok, "fiction"}

    #Quoted string
    iex> Warpath.query(%{"key with whitespace" => "some value"}, "$.['key with whitespace']")
    {:ok, "some value"}

    #Simple atom
    iex> Warpath.query(%{atom_key: "some value"}, "$.:atom_key")
    {:ok, "some value"}

    #Quoted atom
    iex> Warpath.query(%{"atom key": "some value"}, ~S{$.:'atom key'})
    {:ok, "some value"}

    #Unicode support
    iex> Warpath.query(%{"🌢" => "Elixir"}, "$.🌢")
    {:ok, "Elixir"}

    #Union
    iex> document = %{"key" => "value", "another" => "entry"}
    ...> Warpath.query(document, "$['key', 'another']")
    {:ok, ["value", "entry"]}
```

### Children lookup by index
```elixir
    #Positive index
    iex> document = %{"elements" => [:a, :b, :c]}
    ...> Warpath.query(document, "$.elements[0]")
    {:ok, :a}

    #Negative index
    iex> document = %{"elements" => [:a, :b, :c]}
    ...> Warpath.query(document, "$.elements[-1]")
    {:ok, :c}

    #Union
    iex> document = %{"elements" => [:a, :b, :c]}
    ...> Warpath.query(document, "$.elements[0, 1]")
    {:ok, [:a, :b]}
```

### Slice
```elixir
    iex> document = [0, 1, 2, 3, 4]
    ...> Warpath.query(document, "$[0:2:1]")
    {:ok, [0, 1]}

    #optional start and step param.
    iex> document = [0, 1, 2, 3, 4]
    ...> Warpath.query(document, "$[:2]")
    {:ok, [0, 1]}

    #Negative start index
    iex> document = [0, 1, 2, 3, 4]
    ...> Warpath.query(document, "$[-2:]")
    {:ok, [3, 4]}
```

### Filter
```elixir
    # Using logical and operator with is_integer function guard to gain strictness
    iex> document = %{"store" => %{"car" => %{"price" => 100_000}, "bicycle" => %{"price" => 500}}}
    ...> Warpath.query(document, "$..*[?( @.price > 500 and is_integer(@.price) )]")
    {:ok, [%{"price" => 100_000}]}

    # Deep path matching
    iex> addresses = [%{"address" => %{"state" => "Bahia"}}, %{"address" => %{"state" => "São Paulo"}}]
    ...> Warpath.query(addresses, "$[?(@.address.state=='Bahia')]")
    {:ok, [%{ "address" => %{ "state" => "Bahia"}}]}

    #has children using named key
    iex> document = %{"store" => %{"car" => %{"price" => 100_000}, "bicycle" => %{"price" => 500}}}
    ...> Warpath.query(document, "$..*[?(@.price)]")
    {:ok, [%{"price" => 500}, %{"price" => 100_000}]}

    #has children using index
    iex> document = [ [1, 2, 3], [0, 5], [], [1], 9, [9, 8, 7] ]
    ...> Warpath.query(document, "$[?( @[2] )]") # That means give me all list that have index 2.
    {:ok, [ [1, 2, 3], [9, 8, 7]] }
```

### Recursive descendant
```elixir
    #Collect key
    iex> document = %{"store" => %{"car" => %{"price" => 100_000}, "bicycle" => %{"price" => 500}}}
    ...> Warpath.query(document, "$..price")
    {:ok, [500, 100_000]}

    #Collect index
    iex> document = [ [1, 2, 3], [], :item, [0, 5], [1], 9, [9, 8, 7] ]
    ...> Warpath.query(document, "$..[2]")
    {:ok, [:item, 3, 7]}

    #Using filter criteria to scan
    iex> document = [ [1, 2], [], :item, 9, [9, 8], 1.1, "string" ]
    ...> Warpath.query(document, "$..[?( is_list(@) )]")
    {:ok, [ [1, 2], [], [9, 8]]}
```

### Options

```elixir
    
    iex> document = %{"elements" => [:a, :b, :c]}
    ...> Warpath.query(document, "$.elements")
    {:ok, [:a, :b, :c]}

    iex> document = %{"elements" => [:a, :b, :c]}
    ...> Warpath.query(document, "$.elements[0, 1]", result_type: :path)
    {:ok, ["$['elements'][0]", "$['elements'][1]"]}

    iex> document = %{"elements" => [:a, :b, :c]}
    ...> Warpath.query(document, "$.elements[0]", result_type: :path_tokens)
    {:ok, [{:root, "$"}, {:property, "elements"}, {:index_access, 0}]}

    iex> document = %{"elements" => [:a, :b, :c]}
    ...> Warpath.query(document, "$.elements[0, 1]", result_type: :value_path)
    {:ok, [{:a, "$['elements'][0]"}, {:b, "$['elements'][1]"}]}

    iex> document = %{"elements" => [:a, :b, :c]}
    ...> Warpath.query(document, "$.elements[0]", result_type: :value_path_tokens)
    {:ok, {:a, [{:root, "$"}, {:property, "elements"}, {:index_access, 0}]}}

```
<!-- MDOC !-->


**Installation:**

The package can be installed by adding `warpath` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:warpath, "~> 0.6.2"}
  ]
end
```

See documentation at [https://hexdocs.pm](https://hexdocs.pm/warpath/Warpath.html).

To see a comparisions between warpath and others json path libraries, visit [json-path-comparison](https://cburgmer.github.io/json-path-comparison/) and see the great job done by `Christoph Burgmer`.