# chadmespath
A minimal fork of [`jmespath`](https://crates.io/crates/jmespath) 0.3.0 (the Rust
[JMESPath](https://jmespath.org/) implementation by Michael Dowling, MIT) that adds
**one** method — `Expression::search_cached` — so a JSON document can be converted
into JMESPath's value tree **once** and queried many times, instead of being
re-converted on every query.
Everything else is upstream jmespath 0.3, unchanged.
## Why this fork exists
`jmespath::Expression::search` is generic over its input:
```rust
pub fn search<T: ToJmespath>(&self, data: T) -> SearchResult {
let mut ctx = Context::new(&self.expression, self.runtime);
interpret(&data.to_jmespath()?, &self.ast, &mut ctx) // <-- to_jmespath() runs every call
}
```
`data.to_jmespath()` runs on **every** call. When you pass a `serde_json::Value`
(the common case), that dispatches — on stable Rust — to the blanket
`impl<T: serde::Serialize> ToJmespath`, which calls `Variable::from_serializable`:
a full serde walk that allocates a brand-new `Rc<Variable>` tree (one `Rc`, one
`String`, and a `BTreeMap` node per value) for the **whole document**, every time.
So a program that runs *N* JMESPath queries against one document pays that
document-sized conversion *N* times. In a real workload (a scraper extracting a
few dozen fields from each page's embedded JSON) this was measured as **~64% of
all allocations** for a single page — far more than the actual query work.
### Why you can't fix it from the outside
The obvious fix — convert the document to an `Rc<Variable>` once and reuse it — is
not reachable through upstream's public API on stable Rust:
* The zero-cost identity `impl ToJmespath for Rcvar` (which would let `search()`
accept an already-converted tree for free) is gated behind jmespath's
`specialized` feature, which requires `#![feature(specialization)]` —
**nightly only**. On stable, even passing an `Rcvar` to `search()` re-serializes
the whole tree.
* The lower-level `interpret(&Rcvar, &Ast, &mut Context)` that would let you
evaluate against a cached tree lives in a **private** module (`mod interpreter`)
and is not re-exported.
So the cache has nowhere to plug in. This fork adds that plug.
## What it adds
```rust
impl<'a> Expression<'a> {
/// Evaluate against an already-converted `Rcvar` instead of re-converting
/// the input on every call.
pub fn search_cached(&self, data: &Rcvar) -> SearchResult {
let mut ctx = Context::new(&self.expression, self.runtime);
interpret(data, &self.ast, &mut ctx)
}
}
```
Usage — convert once, query many times:
```rust
use chadmespath as jmespath;
use jmespath::{compile, Rcvar, Variable};
// Convert the document to the JMESPath tree exactly once and keep it.
let data: Rcvar = Rcvar::new(Variable::from_json(json_text).unwrap());
for expr_str in selectors {
let expr = compile(expr_str).unwrap();
let result = expr.search_cached(&data).unwrap(); // no document-sized allocation
// ...
}
```
`search_cached` allocates nothing document-sized: the `Rcvar` is shared by
reference and only the query's own results are built.
### Measured effect
Running a 24-selector extraction against one document, upstream `search()` vs.
`search_cached()` with a cached tree:
| 50 specs | 17,902 → **550** | 1,057 KiB → **21 KiB** | 1,377 µs → **40 µs** |
| 200 specs | 65,612 → **1,460** | 3,930 KiB → **64 KiB** | 6,733 µs → **88 µs** |
| 400 specs | 129,217 → **2,665** | 7,766 KiB → **121 KiB** | 12,888 µs → **168 µs** |
Allocations drop ~98% and stop scaling with document size; the small remainder is
just building each query's result strings. CPU per pass improves 35–77×.
## Relationship to upstream
* **Library-only.** Upstream's `build.rs` (which generated the compliance tests
and benches from the JMESPath compliance suite) and the `tests/` and `benches/`
directories are removed. The library source is otherwise upstream 0.3.0.
* **Additive.** `search_cached` is the only API change; `search`, `compile`,
`Variable`, `Runtime`, `Context`, and everything else behave exactly as in
jmespath 0.3.0.
* **Drop-in.** Import it under the `jmespath` alias
(`jmespath = { package = "chadmespath", version = "0.3" }`) and existing
jmespath 0.3 code compiles unchanged; reach for `search_cached` where you have a
cached `Rcvar`.
## License
MIT, preserving the original jmespath.rs copyright. See [LICENSE](LICENSE).