jetro

GitHub

Jetro is a library which provides a custom DSL for transforming, querying and comparing data in JSON format. It is easy to use and extend.

Jetro has minimal dependency, the traversal and eval algorithm is implemented on top of serde_json.

Jetro can be used inside Web Browser by compiling down to WASM. Visit Jetro Web to try it online, or clone it and give it a shot.

Jetro can be used in command line using Jetrocli.

Jetro combines access path with functions which operate on values matched within the pipeline. Access path uses / as separator similar to structure of URI, the start of access path should denote whether the access starts from root by using >, it is possible to traverse from root in nested paths by using <.

Jetro expressions support line breaks and whitespace, the statements can be broken up into smaller parts.

By convention, functions are denoted using # operator. Functions can be composed.

| Function | Action | | -------- | ------ | | #pick('string' \| expression, ...) [ as \| as* 'bindingvalue' ] | Select a key from an object, bind it to a name, select multiple sub queries to create new object | | #head | Head of the list| | #tail | Tail of the list | | #keys | Keys associated with an object | | #values | Values associated with an object | | #reverse | Reverse the list | | #all | Whether all boolean values are true | | #sum | Sum of numbers | | #formats('format with placeholder {} {}', 'keya', 'keyb') [ -> \| ->* 'bindingvalue' ] | Insert formatted key:value into object or return it as single key:value | | #filter('targetkey' (>, <, >=, <=, ==, ~=, !=) (string, boolean, number)) | Perform Filter on list | | #map(x: x.y.z \| x.y.z.somemethod())| Map each item in a list with the given lambda |

```rust let data = serdejson::json!({ "name": "mr snuggle", "someentry": { "someobj": { "obj": { "a": "objecta", "b": "objectb", "c": "objectc", "d": "object_d" } } } });

let mut values = Path::collect(data, ">/..obj/#pick('a','b')");

[derive(Serialize, Deserialize)]

struct Output { a: String, b: String, }

let output: Option = values.from_index(0); ```

structure

Jetro consists of a parser, context wrapper which manages traversal and evaluation of each step of user input and a runtime for dynamic functions. The future version will support user-defined functions.

example

json { "customer": { "id": "xyz", "ident": { "user": { "isExternal": false, "profile": { "firstname": "John", "alias": "Japp", "lastname": "Appleseed" } } }, "preferences": [] }, "line_items": { "items": [ { "ident": "abc", "is_gratis": false, "name": "pizza", "price": 4.8, "total": 1, "type": "base_composable" }, { "ident": "def", "is_gratis": false, "name": "salami", "price": 2.8, "total": 10, "type": "ingredient" }, { "ident": "ghi", "is_gratis": false, "name": "cheese", "price": 2, "total": 1, "type": "ingredient" }, { "ident": "uip", "is_gratis": true, "name": "chilli", "price": 0, "total": 1, "type": "ingredient" }, { "ident": "ewq", "is_gratis": true, "name": "bread sticks", "price": 0, "total": 8, "type": "box" } ] } }

Queries

Get value associated with line_items.

```

/line_items ```

See output

### result json { "items": [ { "ident": "abc", "is_gratis": false, "name": "pizza", "price": 4.8, "total": 1, "type": "base_composable" }, { "ident": "def", "is_gratis": false, "name": "salami", "price": 2.8, "total": 10, "type": "ingredient" }, { "ident": "ghi", "is_gratis": false, "name": "cheese", "price": 2, "total": 1, "type": "ingredient" }, { "ident": "uip", "is_gratis": true, "name": "chilli", "price": 0, "total": 1, "type": "ingridient" }, { "ident": "ewq", "is_gratis": true, "name": "bread sticks", "price": 0, "total": 8, "type": "box" } ] }


Get value associated with first matching key which has a value and return its id field.

```

/('non-existing-member' | 'customer')/id ```

See output

### result

json "xyz"


```

/..items/#tail ```

See output

### result

json [ { "ident": "def", "is_gratis": false, "name": "salami", "price": 2.8, "total": 10, "type": "ingredient" }, { "ident": "ghi", "is_gratis": false, "name": "cheese", "price": 2, "total": 1, "type": "ingredient" }, { "ident": "uip", "is_gratis": true, "name": "chilli", "price": 0, "total": 1, "type": "ingridient" }, { "ident": "ewq", "is_gratis": true, "name": "bread sticks", "price": 0, "total": 8, "type": "box" } ]


```

/..items/#filter('is_gratis' == true and 'name' ~= 'ChILLi') ```

See output

### result

json [ { "ident": "uip", "is_gratis": true, "name": "chilli", "price": 0, "total": 1, "type": "ingridient" } ]


```

/..items/#filter('is_gratis' == true and 'name' ~= 'ChILLi')/#map(x: x.type) ```

See output

### result

json [ "ingridient" ]


Create a new object with scheme {'total': ..., 'fullname': ...} as follow: - recursively search for line_items, dive into any matched object, filter matches with is_gratis == false statement, recursively look for their prices and return the sum of prices - recursively search for object user, select its profile and create a new object with schema {'fullname': ...} formated by concatenating values of keys ('firstname', 'lastname')

```

/#pick( /..lineitems /* /#filter('isgratis' == false)/..price/#sum as 'total',

/..user /profile /#formats('{} {}', 'firstname', 'lastname') ->* 'fullname' ) ```

See output

### result

json { "fullname": "John Appleseed", "total": 9.6 }


Select up to 4 items from index zero of array items

```

/..items/[:4] ```

See output

### result

json [ { "ident": "abc", "is_gratis": false, "name": "pizza", "price": 4.8, "total": 1, "type": "base_composable" }, { "ident": "def", "is_gratis": false, "name": "salami", "price": 2.8, "total": 10, "type": "ingredient" }, { "ident": "ghi", "is_gratis": false, "name": "cheese", "price": 2, "total": 1, "type": "ingredient" }, { "ident": "uip", "is_gratis": true, "name": "chilli", "price": 0, "total": 1, "type": "ingridient" } ]


Select from 4th index and consume until end of array items

```

/..items/[4:] ```

See output

### result

json [ { "ident": "ewq", "is_gratis": true, "name": "bread sticks", "price": 0, "total": 8, "type": "box" } ]


Create a new object with schema {'total_gratis': ...} as follow: - Recursively look for any object containing items, and then recursively search within the matched object for is_gratis and length of matched values

```

/#pick(>/..items/..isgratis/#len as 'totalgratis') ```

See output

### result

json { "total_gratis": 2 }


Recursively search for object items, select its first item and return its keys

```

/..items/[0]/#keys ```

See output

### result

json [ "ident", "is_gratis", "name", "price", "total", "type" ]


Recursively search for object items, select its first item and return its values

```

/..items/[0]/#values ```

See output

### result

json [ "abc", false, "pizza", 4.8, 1, "base_composable" ]