jetro
Jetro is a library for querying, transforming, and comparing JSON. It provides a compact expression language, a bytecode VM with caching.
Jetro has minimal dependencies and operates on top of serde_json.
This project is in experimental phase — significant changes may occur.
Quick start
use Jetro;
use json;
let j = new;
let titles = j.collect.unwrap;
assert_eq!;
One-shot evaluation (tree-walker, no VM caching):
let result = query.unwrap;
Rust API tour
Progressive walk through the Rust API over one rich payload — simple field access through compile-time macros. Every snippet runs against this document:
use *;
let doc = json!;
let j = new;
Jetro holds a thread-local VM; collect() compiles each unique expression once (then reuses the bytecode + pointer cache), so rerunning the same or similar query is essentially free.
1. Field access
assert_eq!;
assert_eq!;
assert_eq!;
2. Filter + map
let cheap = j.collect?;
// ["Dune", "Foundation", "The Hobbit"]
3. Aggregations
let in_stock_total = j.collect?;
let mean_price = j.collect?;
let well_rated = j.collect?;
4. Shaped projections
Build a new object per element:
let summary = j.collect?;
5. Sort, group, membership
let scifi_by_year = j.collect?;
let by_first_tag = j.collect?;
6. Comprehensions
let out_of_stock = j.collect?;
// ["Foundation"]
let id_to_title = j.collect?;
7. Let bindings + f-strings
let headline = j.collect?;
// "Top-rated: The Hobbit (4.83)"
8. Pipelines
| passes the left value through the right expression as @:
let avg = j.collect?;
let shout = j.collect?; // "DUNE"
9. Patch blocks
In-place updates that compose like any other expression:
let discounted = j.collect?;
Patch-field syntax: path: value when predicate?. Paths start with an identifier and may include .field, [n], [*], [* if pred], and ..field. DELETE removes the matched key.
10. Custom methods
Register Rust code as a first-class method:
use ;
use ;
use EvalError;
;
let mut reg = new;
reg.register;
let p90 = query_with?;
11. Typed Expr<T>
Expressions carry a phantom return type:
let titles: = new?;
let upper: = new?;
// `|` composes expressions end-to-end
let pipeline = titles | upper;
let shouted = pipeline.eval?;
12. VM caching
Every query through Jetro::collect or VM::run_str hits two caches:
- Program cache:
expr string → Arc<[Opcode]>. Each unique expression is parsed + compiled exactly once. - Pointer cache: resolved field paths keyed by
(program_id, doc_hash). Structural queries short-circuit straight to the leaves on repeat calls.
Rerunning a workload over fresh documents of the same shape is effectively free after the first call. For one-shot queries use jetro::query(expr, &doc) — it skips the VM entirely.
13. Recursive descent + drill-down chains
$..field walks every descendant level. Combine with filters and projections:
let stats = j.collect?;
let keyed = j.collect?;
14. Cross-join via nested comprehensions
Two generators produce the cartesian product; the if filter keeps only matching pairs:
let receipts = j.collect?;
Three nested loops + three lookups folded into one compiled program. Hits the pointer cache on repeat calls.
15. Layered patch transforms
One patch block stacks path-scoped clauses. Each clause runs on the patched result of the previous one:
let restocked = j.collect?;
Two filter layers: [* if pred] filters per element against the element's own fields; when pred on a field is evaluated against root $. Chain blocks with let ... in patch ... when a later mutation needs to see earlier output.
16. Engine — shared VM across threads
Jetro gives each thread its own VM. Engine is a Send + Sync handle around a Mutex<VM> so one warm cache services many workers:
use *;
use Arc;
let engine = new; // Arc<Engine>
let shared = clone;
spawn.join.unwrap;
First call compiles and fills both caches; every subsequent thread hitting the same expression skips to execution.
17. Compile-time expressions — jetro!
Enable the macros feature. Expressions get a full pest parse at compile time, with errors pointing at the exact macro call-site. Returns a typed Expr<Value>:
use jetro;
let avg_price = jetro!;
let n_classic = jetro!;
assert_eq!;
assert_eq!;
Unbalanced parens, unterminated strings, unknown operators — build failures, not runtime errors.
18. #[derive(JetroSchema)] — attribute-driven schemas
Pair a type with a fixed set of named expressions. The derive emits EXPRS, exprs(), and names(). Each #[expr(...)] line is grammar-checked at compile time, so an invalid expression in a schema never ships.
use JetroSchema;
;
assert_eq!;
for in exprs
Feature flags
| Feature | Pulls in | Unlocks |
|---|---|---|
macros |
jetro-macros companion crate |
jetro!(...) macro, #[derive(JetroSchema)] |
Cargo.toml:
[]
= { = "0.3", = ["macros"] }
Syntax
Root and context
| Token | Meaning |
|---|---|
$ |
Document root |
@ |
Current item (lambdas, pipes, comprehensions, patch paths) |
Navigation
$.field // child field
$.a.b.c // nested
$.user?.name // null-safe field
$.items[0] // array index
$.items[-1] // last
$.items[2:5] // slice [2, 5)
$.items[2:] // tail
$.items[:5] // head
$..title // recursive descent
Literals
null true false
42 3.14
"hello" 'world'
f"Hello {$.user.name}!" // f-string with interpolation
f"{$.price:.2f}" // format spec
f"{$.name | upper}" // pipe transform
Operators
== != < <= > >= // comparison
~= // fuzzy match (case-insensitive substring)
+ - * / % // arithmetic
and or not // logical
$.field ?| "default" // null coalescing
Methods
All operations are dot-called methods:
$.books.filter(price > 10)
$.books.map(title)
$.books.sum(price)
$.books.filter(price > 10).count()
$.books.sort_by(price).reverse()
$.users.group_by(tier)
$.items.map({name, total: price * qty})
Comprehensions
[book.title for book in $.books if book.price > 10]
{user.id: user.name for user in $.users if user.active}
Let bindings
let top = $.books.filter(price > 100) in
{count: top.len(), titles: top.map(title)}
Lambdas
$.books.filter(lambda b: b.tags.includes("sci-fi"))
Kind checks
$.items.filter(price kind number and price > 0)
$.data.filter(deleted_at kind null)
Pipelines
| passes the left value through the right expression as @:
$.name | upper
$.price | (@ * 1.1)
Patch blocks
In-place updates with block-form syntax. Each clause matches a path and produces either a replacement value or DELETE:
patch $ {
books[*].price: @ * 1.1,
books[* if stock == 0]: DELETE,
meta.updated_at: now()
}
Architecture
Expression engine
Everything below is shipped by the jetro-core crate and re-exported here.
grammar.pest+parser.rs— pest PEG grammar →ast::Expreval/mod.rs— tree-walking evaluator (reference semantics)eval/value.rs—Valtype:Arc-wrapped compound nodes, O(1) cloneseval/func_*.rs— built-in methods grouped by domain (strings, arrays, objects, paths, aggregates, csv)eval/methods.rs—Methodtrait +MethodRegistryfor user-registered methodsvm.rs— bytecode compiler + stack machine; 10 peephole passes; compile and pointer cachesgraph.rs— multi-document query primitive (merges named nodes into a virtual root)analysis.rs/schema.rs/plan.rs/cfg.rs/ssa.rs— optional IRs (type/nullness/cardinality, shape inference, logical plan, CFG, SSA numbering)
See jetro-core/README.md for a full technical walkthrough: AST variants, opcode set, peephole passes, cache invariants.
Companion crates
jetro-core— engine (this crate re-exports it)jetro-macros—jetro!()+#[derive(JetroSchema)](feature = "macros")
License
MIT. See LICENSE.