@jarenjs/json 0.9.2 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +86 -13
- package/README.md +248 -23
- package/dist/types/canonical.d.ts +37 -0
- package/dist/types/cow.d.ts +28 -0
- package/dist/types/errors.d.ts +45 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/jslt/errors.d.ts +15 -8
- package/dist/types/jslt/index.d.ts +22 -0
- package/dist/types/jslt/packs/finance.d.ts +119 -0
- package/dist/types/jslt/packs/index.d.ts +310 -0
- package/dist/types/jslt/packs/math.d.ts +159 -0
- package/dist/types/jslt/packs/stats.d.ts +48 -0
- package/dist/types/jslt/registry.d.ts +65 -0
- package/dist/types/jtlt/errors.d.ts +3 -6
- package/dist/types/option-variants.d.ts +29 -0
- package/dist/types/patch.d.ts +214 -0
- package/dist/types/path.d.ts +139 -9
- package/dist/types/pointer.d.ts +100 -9
- package/dist/types/query/compile.d.ts +12 -0
- package/dist/types/query/errors.d.ts +72 -8
- package/dist/types/query/index.d.ts +317 -25
- package/dist/types/query/normalize.d.ts +24 -0
- package/dist/types/query/operators.d.ts +241 -1
- package/dist/types/query/runtime.d.ts +5 -8
- package/dist/types/query/types.d.ts +34 -0
- package/dist/types/segments.d.ts +31 -0
- package/dist/types/write.d.ts +204 -0
- package/dist/types/xquery/parse.d.ts +2 -3
- package/docs/JSLT-FORMAT.md +74 -3
- package/docs/JSLT-PRELUDE.md +1 -1
- package/docs/QUERY-FORMAT.md +695 -33
- package/package.json +18 -4
- package/schemas/geojson.draft-07.schema.json +323 -0
- package/schemas/geojson.jaren.schema.json +863 -0
- package/schemas/geojson.schema.json +172 -0
- package/schemas/jaren-jslt.authoring.schema.json +142 -0
- package/schemas/jaren-jslt.draft-07.schema.json +152 -11
- package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
- package/schemas/jaren-jslt.schema.json +152 -11
- package/schemas/jaren-query.draft-07.schema.json +152 -11
- package/schemas/jaren-query.llm-profile.schema.json +619 -0
- package/schemas/jaren-query.schema.json +82 -15
- package/src/basic.js +1 -1
- package/src/canonical.js +170 -0
- package/src/cow.js +106 -0
- package/src/errors.js +68 -0
- package/src/index.js +3 -0
- package/src/jslt/dispatch.js +178 -28
- package/src/jslt/errors.js +19 -14
- package/src/jslt/index.js +37 -29
- package/src/jslt/packs/finance.js +49 -0
- package/src/jslt/packs/index.js +18 -0
- package/src/jslt/packs/math.js +46 -0
- package/src/jslt/packs/stats.js +65 -0
- package/src/jslt/registry.js +200 -0
- package/src/jslt/stylesheet.js +14 -23
- package/src/jtlt/desugar.js +2 -3
- package/src/jtlt/errors.js +6 -12
- package/src/jtlt/index.js +12 -29
- package/src/jtlt/template.js +9 -18
- package/src/option-variants.js +54 -0
- package/src/patch.js +1052 -0
- package/src/path.js +319 -52
- package/src/pointer.js +225 -44
- package/src/query/compile.js +790 -75
- package/src/query/errors.js +72 -12
- package/src/query/index.js +274 -42
- package/src/query/normalize.js +489 -78
- package/src/query/operators.js +620 -23
- package/src/query/runtime.js +5 -19
- package/src/query/types.js +213 -0
- package/src/segments.js +409 -64
- package/src/write.js +660 -0
- package/src/xquery/parse.js +37 -53
package/README.md
CHANGED
|
@@ -8,9 +8,11 @@ None of it depends on JSON Schema: every module can be used standalone in any Ja
|
|
|
8
8
|
|
|
9
9
|
| Import | Contents |
|
|
10
10
|
|---|---|
|
|
11
|
-
| `@jarenjs/json` |
|
|
11
|
+
| `@jarenjs/json` | the addressing and query modules below (`jslt`/`jtlt`/`xquery` are subpath-only) |
|
|
12
12
|
| `@jarenjs/json/basic` | JSON, JSON Pointer and JSONPath string validation |
|
|
13
13
|
| `@jarenjs/json/pointer` | the JSON Pointer and Relative JSON Pointer compiler |
|
|
14
|
+
| `@jarenjs/json/patch` | JSON Patch and JSON Merge Patch: compiled apply + structural diff |
|
|
15
|
+
| `@jarenjs/json/write` | compiled write operations: set/insert/remove at pointers, normalized paths, or every JSONPath match |
|
|
14
16
|
| `@jarenjs/json/path` | the JSONPath compiler |
|
|
15
17
|
| `@jarenjs/json/query` | the Jaren JSON Query engine |
|
|
16
18
|
| `@jarenjs/json/jslt` | the Jaren JSLT stylesheet compiler and dispatcher |
|
|
@@ -24,9 +26,14 @@ None of it depends on JSON Schema: every module can be used standalone in any Ja
|
|
|
24
26
|
| Standard | Functions |
|
|
25
27
|
|---|---|
|
|
26
28
|
| JSON validation | `isValidJSON`, `isValidJSONCheap` (a fast "definitely not JSON" pre-test) |
|
|
27
|
-
| JSON Pointer ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)) | `compileJSONPointer`, `parseJSONPointer`, `isValidJSONPointer`, `isValidJSONPointerUriFragment` |
|
|
29
|
+
| JSON Pointer ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)) | `compileJSONPointer`, `parseJSONPointer`, `parseJSONPointerPath`, `isValidJSONPointer`, `isValidJSONPointerUriFragment` |
|
|
28
30
|
| Relative JSON Pointer | `compileRelativeJSONPointer`, `parseRelativeJSONPointer`, `compileDataRef`, `isValidRelativeJSONPointer` |
|
|
29
|
-
|
|
|
31
|
+
| JSON Patch ([RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902)) | `compileJSONPatch`, `applyJSONPatch`, `createJSONPatch`, `isValidJSONPatch`, `JsonPatchCompileError`, `JsonPatchRuntimeError` |
|
|
32
|
+
| JSON Merge Patch ([RFC 7396](https://datatracker.ietf.org/doc/html/rfc7396)) | `compileMergePatch`, `applyMergePatch`, `createMergePatch` |
|
|
33
|
+
| Write operations | `compileJSONPointerSetter`/`Inserter`/`Remover`, `compileJSONPathSetter`/`Inserter`/`Remover`, one-shot `setAtJSONPointer`, `removeAtJSONPath`, ..., `JsonWriteError` |
|
|
34
|
+
| Addressing bridge | `jsonPointerFromJSONPath`, `jsonPathFromJSONPointer` |
|
|
35
|
+
| JSONPath ([RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html)) | `compileJSONPath`, `queryJSONPath`, `parseJSONPath`, `isValidJSONPathStrict`, `isValidJSONPathSegments` |
|
|
36
|
+
| Canonical JSON ([RFC 8785](https://www.rfc-editor.org/rfc/rfc8785.html)) | `canonicalizeJson`, `JsonCanonicalizeError` |
|
|
30
37
|
| Jaren JSON Query | `compileJsonQuery`, `queryJson`, `JsonQueryCompileError`, `JsonQueryRuntimeError` |
|
|
31
38
|
| Jaren JSLT | `compileJsltStylesheet`, `transformJson`, `JsltCompileError`, `JsltRuntimeError` |
|
|
32
39
|
| Jaren JTLT | `compileJtltStylesheet`, `renderText`, `JtltCompileError`, `JtltRuntimeError` |
|
|
@@ -57,8 +64,118 @@ const getName = compileRelativeJSONPointer('0#');
|
|
|
57
64
|
getName(doc, '/limits/min'); // 'min' (the member name of the location)
|
|
58
65
|
```
|
|
59
66
|
|
|
67
|
+
The `#` form has two modes, chosen once at compile time, because the spec's
|
|
68
|
+
answer and the fast answer are not the same thing. Relative JSON Pointer says
|
|
69
|
+
`#` yields the member *name* for an object member and the *index* — a number —
|
|
70
|
+
for an array element, and telling those apart means looking at the container:
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
const asString = compileRelativeJSONPointer('0#'); // default
|
|
74
|
+
const asNumber = compileRelativeJSONPointer('0#', { hashIndex: 'number' });
|
|
75
|
+
|
|
76
|
+
asString({ a: ['x', 'y'] }, '/a/1'); // '1' — string, answered from the path alone
|
|
77
|
+
asNumber({ a: ['x', 'y'] }, '/a/1'); // 1 — number, the draft's answer
|
|
78
|
+
asNumber({ o: { 1: 'v' } }, '/o/1'); // '1' — a member named "1" is still a name
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`hashIndex: 'string'` is the default and never touches the document, which is
|
|
82
|
+
what keeps it a string operation of tens of nanoseconds; it is also what the
|
|
83
|
+
validator's `$data` keyword has always seen, so `{"$data": "0#"}` compared
|
|
84
|
+
against a number-typed keyword gets a string. `hashIndex: 'number'` walks to the
|
|
85
|
+
parent of the location to check whether it is an array, and falls back to the
|
|
86
|
+
string when that parent cannot be reached — nothing proves a position is an
|
|
87
|
+
index. An unknown mode is a `TypeError` at compile time rather than a silent
|
|
88
|
+
fallback. `compileDataRef` forwards the option.
|
|
89
|
+
|
|
90
|
+
Neither mode verifies the location itself exists: the caller passes a location
|
|
91
|
+
it actually reached. The root has no name, so `0#` there is
|
|
92
|
+
`JSONPOINTER_NOTHING` and stays distinguishable from the member a document can
|
|
93
|
+
genuinely name `''`.
|
|
94
|
+
|
|
60
95
|
`compileDataRef(ref)` compiles the union the validator accepts — `''` for the data root, a leading `/` for an absolute pointer, a leading digit for a relative one — deciding the dispatch once at compile time. On a realistic `$data` workload the compiled resolvers are 4–20x faster than the interpretive resolver they replaced, and beat the `jsonpointer` npm package on every scenario (`npm run benchmark:jsonpointer`, 2026-07-17: absolute pointers 12–16x, relative pointers 4–16x, `compileDataRef` dispatch 7–20x).
|
|
61
96
|
|
|
97
|
+
The write-side encode is there too: `encodeJSONPointerSegment(key)` escapes one reference token (`~` → `~0`, `/` → `~1`), `decodeJSONPointerSegment(token)` is its inverse for one token (decoding `~1` before `~0`, so a member named `~1` survives the round trip), and `formatJSONPointer(segments)` is the inverse of `parseJSONPointer`.
|
|
98
|
+
|
|
99
|
+
`parseJSONPointerPath(pointer)` is the typed variant: it returns
|
|
100
|
+
`(string|number)[]`, narrowing canonical array indexes to numbers
|
|
101
|
+
(`'/items/0/id'` → `['items', 0, 'id']`) while leaving anything RFC 6901 does
|
|
102
|
+
not make an index — `01`, `1e0`, `-`, `1abc` — a string. RFC 6901 has no
|
|
103
|
+
types, so the classification is lexical rather than resolved against a
|
|
104
|
+
document, using the same index rule the pointer compiler uses. This is the
|
|
105
|
+
path shape error reporters and diffing tools expect, so it pairs directly
|
|
106
|
+
with a validation error's `instancePath`.
|
|
107
|
+
|
|
108
|
+
### JSON Patch and JSON Merge Patch
|
|
109
|
+
|
|
110
|
+
Partial updates follow the same compile-once discipline. `compileJSONPatch(patch)` validates an [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) patch document once (`JsonPatchCompileError`, `JP0xxx`, with a `docPath` pointing into the *patch* document), pre-parses every `path`/`from` through the RFC 6901 parser and specializes one closure per operation. Applying is **copy-on-write**: the input document is never mutated, untouched subtrees are shared by reference with the result, and application is atomic — a failing operation (`JsonPatchRuntimeError`, `JP2xxx`, carrying both the patch `docPath` and the target `dataPath`) leaves nothing behind, exactly as RFC 6902 section 5 requires. The engine passes the complete official [json-patch-tests](https://github.com/json-patch/json-patch-tests) suite:
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
import { compileJSONPatch, applyJSONPatch, createJSONPatch } from '@jarenjs/json';
|
|
114
|
+
|
|
115
|
+
// compile once, apply many times (hot path)
|
|
116
|
+
const apply = compileJSONPatch([
|
|
117
|
+
{ op: 'test', path: '/version', value: 5 },
|
|
118
|
+
{ op: 'replace', path: '/user/name', value: 'Bob' },
|
|
119
|
+
{ op: 'add', path: '/user/tags/-', value: 'admin' },
|
|
120
|
+
]);
|
|
121
|
+
const next = apply(doc); // doc is untouched; unchanged subtrees are shared
|
|
122
|
+
|
|
123
|
+
// one-shot
|
|
124
|
+
applyJSONPatch(doc, patch);
|
|
125
|
+
|
|
126
|
+
// structural diff: a change feed for @jarenjs/forms and friends
|
|
127
|
+
applyJSONPatch(a, createJSONPatch(a, b)); // deep-equals b
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Because an application only clones the spine it writes through — and clones it once, no matter how many operations touch the same region — the compiled applier beats the usual clone-and-interpret shape by 5–170x depending on document size (`npm run benchmark:jsonpatch`). Two options tune the copy discipline, mirroring JSLT's `share`/`fresh` dispositions: `values: 'fresh'` deep-copies inserted operation values per application (the default `'share'` inserts them by reference, so treat results as immutable), and `mutate: true` patches in place for the last bit of speed at the cost of atomicity.
|
|
131
|
+
|
|
132
|
+
`createJSONPatch(source, target)` trims each array's deep-equal common prefix and suffix and pairs the rest up index-wise, which is linear and already minimal for in-place edits and head/tail insertions — but turns a mid-array insertion into a run of per-index replaces. `createJSONPatch(source, target, { arrayDiff: 'minimal' })` aligns the changed middle instead, so the insertion is one `add`:
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
const before = [{ id: 1 }, { id: 2 }, { id: 3, n: 0 }];
|
|
136
|
+
const after = [{ id: 1 }, { id: 9 }, { id: 2 }, { id: 3, n: 1 }];
|
|
137
|
+
|
|
138
|
+
createJSONPatch(before, after);
|
|
139
|
+
// 4 ops: replace /1/id, replace /2/id, remove /2/n, add /3
|
|
140
|
+
|
|
141
|
+
createJSONPatch(before, after, { arrayDiff: 'minimal' });
|
|
142
|
+
// 2 ops: add /1 {id:9}, replace /3/n 1
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The alignment minimizes the patch itself (edit distance with substitutions — *not* a longest common subsequence, which maximizes kept elements and so pays a delete plus an insert on a permutation where one rewrite would do), and therefore never takes more steps than the default. It stays opt-in because it is O(m·n) in the length of the changed middle; past a fixed budget a single array falls back to the linear diff, so the option can never make a large diff quadratic. Both modes reproduce `target` exactly.
|
|
146
|
+
|
|
147
|
+
A third option makes the engine a **change feed**: `changes: true` specializes the applier to return `{ doc, changes }`, where `changes` holds one JSON Pointer per successful write, in application order. The reported pointers are *invalidation-sound* — object writes, array replaces and appends report the written location itself; shifting array inserts and removes report the parent array; a root write reports `''` — which is exactly the primitive dirty-path consumers (view re-rendering in `@jarenjs/app`, rule dependency memoization in `@jarenjs/forms`) need. `test` operations report nothing, and the option costs nothing when off: the tracking branch is compiled out of the applier.
|
|
148
|
+
|
|
149
|
+
[RFC 7396](https://datatracker.ietf.org/doc/html/rfc7396) merge patches ride the same machinery: `compileMergePatch(patch)` pre-splits the patch into remove/set/merge plans, and applying is identity-preserving — a merge that changes nothing returns the target by reference, so it doubles as a cheap change detector. `createMergePatch(source, target)` emits the merge patch (with the RFC's documented `null`-member representability caveat), and `applyMergePatch(doc, patch)` is the one-shot form.
|
|
150
|
+
|
|
151
|
+
### Write operations
|
|
152
|
+
|
|
153
|
+
When a whole patch document is more ceremony than the job needs, the standalone write operations expose the same copy-on-write core directly — `set`, `insert` and `remove`, compiled once per target:
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
import {
|
|
157
|
+
compileJSONPointerSetter, removeAtJSONPointer,
|
|
158
|
+
compileJSONPathSetter, removeAtJSONPath,
|
|
159
|
+
} from '@jarenjs/json';
|
|
160
|
+
|
|
161
|
+
// a target is an RFC 6901 pointer, a normalized path, or ANY singular
|
|
162
|
+
// query — '/store/book/0/title', "$['store']['book'][0]['title']" and
|
|
163
|
+
// '$.store.book[-1].title' (negative = from the end) all compile
|
|
164
|
+
const setZip = compileJSONPointerSetter('/address/zip');
|
|
165
|
+
const next = setZip(doc, '10999'); // doc untouched, spine cloned once
|
|
166
|
+
|
|
167
|
+
setZip(doc, (old) => old ?? '10115'); // setters take updater functions
|
|
168
|
+
|
|
169
|
+
// ...or write at EVERY node a JSONPath query selects
|
|
170
|
+
const addVat = compileJSONPathSetter('$..price');
|
|
171
|
+
addVat(doc, (price) => price * 1.21);
|
|
172
|
+
removeAtJSONPath(doc, '$.store.book[?@.price > 20]');
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Set replaces (creating a missing final member; `/arr/-` appends), insert has RFC 6902 `add` semantics (array elements shift right), remove deletes with shift. The query-selected writers apply matched locations in **reverse document order**, so multiple removals or inserts in one array — and nested matches — compose without index bookkeeping, and matching nothing is a no-op that returns the input. Everything is copy-on-write with an in-place `{ mutate: true }` escape hatch; failures (`JsonWriteError`, `JW0001`/`JW2xxx` with the target as `dataPath`) leave the input untouched.
|
|
176
|
+
|
|
177
|
+
The addressing bridge rounds this out: `jsonPointerFromJSONPath(singularQuery)` and `jsonPathFromJSONPointer(pointer)` convert between the two location languages (digit tokens become index selectors — RFC 6901's one-token-two-forms ambiguity, resolved by convention), so pointers, normalized paths and query results compose freely.
|
|
178
|
+
|
|
62
179
|
## The JSONPath compiler
|
|
63
180
|
|
|
64
181
|
JSONPath selects *many* locations: a query like `$.store.book[?@.price < 10].title` describes a whole nodelist. The `path.js` module implements the complete RFC 9535 specification as a two-stage compiler, mirroring the philosophy of the schema validator: **parse and decide everything once, then run a specialized function**.
|
|
@@ -81,6 +198,7 @@ query(other); // compiled once, reusable on any document
|
|
|
81
198
|
| `query(data)` / `query.values(data)` | array of matched values, in document order |
|
|
82
199
|
| `query.first(data)` | the first matched value, or `undefined` |
|
|
83
200
|
| `query.exists(data)` | `true` when at least one node matches |
|
|
201
|
+
| `query.iterate(data)` | a generator yielding matched values on demand, in document order |
|
|
84
202
|
| `query.nodes(data)` | array of `{ path, value }` pairs with normalized paths |
|
|
85
203
|
| `query.paths(data)` | array of normalized paths (RFC 9535 §2.7), e.g. `$['store']['book'][0]['title']` |
|
|
86
204
|
| `query.source` | the original query string |
|
|
@@ -88,6 +206,29 @@ query(other); // compiled once, reusable on any document
|
|
|
88
206
|
|
|
89
207
|
For one-off queries there is `queryJSONPath(source, data)`, which keeps a cache of compiled queries (512 entries, FIFO), and `isValidJSONPathStrict(source)` for a boolean grammar check — this is what the `json-path` format in [`@jarenjs/formats`](../formats) uses.
|
|
90
208
|
|
|
209
|
+
`first`, `exists` and `iterate` are one lazy walk: they stop at the first node they need instead of building the whole nodelist, so `q.first(doc)` on `$.items[?@.price < 10].name` over 10 000 items costs about 2 µs where `q.values(doc)` costs about 240 µs. Nothing is buffered along the way — a filter evaluates its predicate only until one passes, and a descendant segment abandons the walk mid-subtree.
|
|
210
|
+
|
|
211
|
+
#### Custom function extensions
|
|
212
|
+
|
|
213
|
+
`compileJSONPath(source, { pathFunctions })` registers function extensions ([RFC 9535 §2.4](https://www.rfc-editor.org/rfc/rfc9535.html#name-function-extensions)) alongside the five built-ins. An entry declares its parameter and result types, which is what lets the parser type-check call sites the same way it checks `length()` or `match()`:
|
|
214
|
+
|
|
215
|
+
```js
|
|
216
|
+
const pathFunctions = {
|
|
217
|
+
is_even: {
|
|
218
|
+
params: ['value'], // 'value' | 'nodes' | 'logical'
|
|
219
|
+
returns: 'logical',
|
|
220
|
+
evaluate: (v) => typeof v === 'number' && v % 2 === 0,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const q = compileJSONPath('$.items[?is_even(@.n)].n', { pathFunctions });
|
|
225
|
+
q({ items: [{ n: 1 }, { n: 2 }, { n: 3 }, { n: 4 }] }); // [2, 4]
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
A `value` parameter arrives as a JSON value or `JSONPATH_NOTHING`, a `nodes` parameter as an array of the selected values, a `logical` parameter as a boolean — and a `logical` parameter accepts the full filter grammar at the call site, so `both(@.a > 1, !@.b)` parses. A name that would redefine a built-in is rejected (§2.4.1), and a malformed registry raises a `TypeError`.
|
|
229
|
+
|
|
230
|
+
The same option reaches everywhere a path string is embedded: the JSONPath-addressed writers, `compileJsonQuery`, and JSLT match paths and rule bodies. The `json-path` and `json-path-segments` string formats deliberately do **not** see it — a format is a property of the string, so it has to mean the same thing in every schema regardless of which extensions a host installed.
|
|
231
|
+
|
|
91
232
|
### Supported syntax
|
|
92
233
|
|
|
93
234
|
Everything in RFC 9535, with no extensions and no omissions:
|
|
@@ -211,17 +352,72 @@ One phrase carries the whole pipeline. Clauses apply in **fixed semantic order r
|
|
|
211
352
|
|
|
212
353
|
| Key | Value | Presence |
|
|
213
354
|
|---|---|---|
|
|
214
|
-
| `$
|
|
215
|
-
| `$
|
|
355
|
+
| `$fold` | one accumulator binding `{ name: initExpr }` | at least one of `$fold`/`$for`/`$let` |
|
|
356
|
+
| `$for` | iteration bindings `{ name: source, ... }`, extended form `{ "$in": expr, "$at": "i" }` | at least one of `$fold`/`$for`/`$let` |
|
|
357
|
+
| `$let` | sequence bindings (no iteration) | at least one of `$fold`/`$for`/`$let` |
|
|
358
|
+
| `$as` | schema assertions on this phrase's bindings | optional |
|
|
216
359
|
| `$where` | tuple filter (effective boolean value) | optional |
|
|
217
360
|
| `$groupby` | grouping-key bindings | optional |
|
|
218
361
|
| `$orderby` | key spec or list: `{ "$key": e, "$dir": "desc", "$empty": "greatest" }` | optional |
|
|
219
362
|
| `$count` | variable name for the 0-based tuple index | optional |
|
|
220
363
|
| `$return` | the result expression per surviving tuple | required |
|
|
221
364
|
|
|
222
|
-
Semantic order: `$for → $let → $where → $groupby → $orderby → $count → $return`. Quantifiers are their own two-key phrases: `{"$some": bindings, "$satisfies": expr}` / `{"$every": ...}`, with short-circuit evaluation.
|
|
365
|
+
Semantic order: `$fold → $for → $let → $as → $where → $groupby → $orderby → $count → $return`. Quantifiers are their own two-key phrases: `{"$some": bindings, "$satisfies": expr}` / `{"$every": ...}`, with short-circuit evaluation.
|
|
366
|
+
|
|
367
|
+
**`$fold` turns the phrase into a reduction.** The accumulator's initial value is evaluated once, `$return` names its next value per surviving tuple, and the phrase evaluates to the final accumulator instead of the collected sequence. This is how the language gets a general fold without the JSON encoding needing to spell a *function value* — the accumulator is a binding, not a lambda parameter. It composes with the rest, so `$orderby` folds over sorted tuples and `$groupby` updates once per group:
|
|
368
|
+
|
|
369
|
+
```js
|
|
370
|
+
queryJson({
|
|
371
|
+
$fold: { total: 0 },
|
|
372
|
+
$for: { b: '$.store.book[*]' },
|
|
373
|
+
$where: { $lt: ['$b.price', 10] },
|
|
374
|
+
$return: { $add: ['$total', '$b.price'] },
|
|
375
|
+
}, bookstore); // 17.939999999999998 — every number is an IEEE double (D1)
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Because `$get` is a real dynamic lookup, a fold over a runtime path is a pointer walk: `{"$fold": {"cur": "$.doc"}, "$for": {"seg": "$.path[*]"}, "$return": {"$get": ["$cur", "$seg"]}}`.
|
|
223
379
|
|
|
224
|
-
|
|
380
|
+
**Extended `$for` bindings** cover the two remaining XQuery iteration shapes. `{"$in": e, "$allowing-empty": true}` is outer-join iteration: when the source yields no tuple, one tuple is emitted with the variable bound to the empty sequence (position `-1` if `$at` is present), so the enclosing row survives. `{"$in": e, "$window": "tumbling"|"sliding", "$size": n, "$step": m}` iterates runs instead of items — a tumbling window *partitions* the stream, so its short final window is kept; a sliding window is a fixed-width moving view, so only full windows are emitted.
|
|
381
|
+
|
|
382
|
+
```js
|
|
383
|
+
queryJson({
|
|
384
|
+
$for: { w: { $in: '$[*]', $window: 'sliding', $size: 3 } },
|
|
385
|
+
$return: { $avg: '$w' },
|
|
386
|
+
}, [1, 2, 3, 4, 5]); // [2, 3, 4] — a 3-point moving average
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The operator library (93 operators: comparisons, IEEE-double arithmetic, logic, strings with I-Regexp `$match`/`$search`/`$replace`, aggregates, sequence tools like `$distinct`/`$subsequence`/`$range`, type predicates and casts, `$coalesce`, the RFC 3339 date family, and the spatial family) is cataloged in [QUERY-FORMAT.md §8](./docs/QUERY-FORMAT.md#8-operators).
|
|
390
|
+
|
|
391
|
+
**Extending the vocabulary (host opt-in).** The 93 are closed, but a host can add more the way `@jarenjs/validate` gains formats from `@jarenjs/formats`: `createJsltRegistry().use(mathPack).use(financePack)` composes packs of pure `@jarenjs/core` functions into a compiler, so `{ "$sqrt": "$.variance" }` and `{ "$npv": ["$.rate", "$.cashflows[*]"] }` work in a stylesheet, a bare query, and `@jarenjs/linq` — while a document compiled *without* the registry still rejects them. Aggregators fold a `seq` operand to an array before the pure call. See [JSLT-FORMAT.md §13](./docs/JSLT-FORMAT.md#13-registered-operators-host-opt-in-non-normative).
|
|
392
|
+
|
|
393
|
+
**Dates are RFC 3339 strings** ([§8.13](./docs/QUERY-FORMAT.md#813-dates-and-times)): `$is-date`/`$is-time`/`$is-datetime`/`$is-duration` test the lexical forms, `$year`…`$seconds` and `$offset` read components *lexically, in the value's own offset* (so "group by month" means what you expect), `$week`/`$week-year`/`$quarter`/`$weekday` add the derived calendar fields, and `$epoch`/`$datetime` convert to and from epoch milliseconds — the one place a value is shifted to UTC, and therefore the way to compare instants across offsets. There is deliberately no `current-dateTime`: a compiled query is cached by document identity and saved as a rule, so it must answer the same for the same input forever.
|
|
394
|
+
|
|
395
|
+
Dates also *move*. `$date-add`/`$date-sub` shift by an ISO 8601 duration or by an amount and a unit, `$start-of`/`$end-of` truncate to a calendar unit, `$date-diff` counts whole units, and `$date-format` renders through a Unicode LDML pattern compiled once with the query. Two rules keep it predictable: the **lexical form is preserved** (a date stays a date, and a date-time keeps its own offset instead of being normalized to UTC), and **month arithmetic clamps**, so 31 January plus a month is 28 February — with `$date-diff` counting to match, so adding its answer back never overshoots.
|
|
396
|
+
|
|
397
|
+
```js
|
|
398
|
+
queryJson({
|
|
399
|
+
$for: { e: '$[*]' },
|
|
400
|
+
$groupby: { w: { '$start-of': ['$e.on', 'week'] } },
|
|
401
|
+
$orderby: ['$w'],
|
|
402
|
+
$return: { week: '$w', count: { $count: '$e' } },
|
|
403
|
+
}, [{ on: '2026-01-05' }, { on: '2026-01-08' }, { on: '2026-01-20' }]);
|
|
404
|
+
// [{ week: '2026-01-05', count: 2 }, { week: '2026-01-19', count: 1 }]
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Bucketing by week is the shape components alone cannot express, because a week boundary is arithmetic rather than a field. Patterns stay locale-independent — `MMMM` and `EEEE` are rejected rather than silently rendered in English — because localized text belongs to the presentation layer, not to a query.
|
|
408
|
+
|
|
409
|
+
**Geography is GeoJSON** ([§8.14](./docs/QUERY-FORMAT.md#814-spatial)), for the same reason dates are RFC 3339 strings: it is what the document already holds. Operands are a bare `[longitude, latitude]` position, a geometry, a `Feature` or a `FeatureCollection`, and wrappers unwrap for you. `$distance`, `$area` and `$length` answer in metres on the WGS 84 sphere — never planar, because a Euclidean answer over raw degrees is wrong by two thirds over a kilometre at Dutch latitudes. `$within` tests containment, `$bbox`/`$centroid` measure, and `$bbox-intersects` is named for exactly what it tests, since an `$intersects` that compared only boxes would be a lie the first time two L-shapes shared one.
|
|
410
|
+
|
|
411
|
+
```js
|
|
412
|
+
queryJson({
|
|
413
|
+
$for: { c: '$.cities[*]' },
|
|
414
|
+
$where: { $within: ['$c.at', '$.region'] },
|
|
415
|
+
$orderby: [{ $key: { $distance: ['$c.at', '$.centre'] } }],
|
|
416
|
+
$return: '$c.name',
|
|
417
|
+
}, data); // a spatial filter and a spatial sort, in the language's own clauses
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
A geohash needs no operator of its own to be useful: it is a string, so proximity is `$starts-with` on a prefix and spatial bucketing is `$groupby` over `$substring`. `$geohash` only produces it.
|
|
225
421
|
|
|
226
422
|
### External parameters
|
|
227
423
|
|
|
@@ -259,6 +455,28 @@ The complete structural grammar of the language is published as JSON Schema, twi
|
|
|
259
455
|
|
|
260
456
|
Both exist because the schema is authored in a draft-neutral keyword subset (no `$ref` siblings, no `unevaluated*`, no tuples), so ecosystems pinned to draft-07 — several structured-output stacks among them — get an identical grammar for one `$defs`→`definitions` rename. Every fixture in the test suite validates under both drafts.
|
|
261
457
|
|
|
458
|
+
### GeoJSON, fully validated
|
|
459
|
+
|
|
460
|
+
The same machinery answers a problem the geospatial ecosystem has lived with for years. The [official GeoJSON JSON Schema](https://github.com/geojson/schema) states that it **cannot** express that a linear ring is closed, or that rings follow RFC 7946's right-hand rule — neither is a structural property — so every validator bolts on custom code, and unclosed rings and reversed winding (which makes a renderer fill the entire globe) remain among the commonest defects in real data.
|
|
461
|
+
|
|
462
|
+
Three artifacts ship here:
|
|
463
|
+
|
|
464
|
+
- [`schemas/geojson.schema.json`](./schemas/geojson.schema.json) — **portable**: plain draft-neutral JSON Schema, no Jaren extension, usable by any validator;
|
|
465
|
+
- [`schemas/geojson.draft-07.schema.json`](./schemas/geojson.draft-07.schema.json) — its mechanically derived draft-07 twin;
|
|
466
|
+
- [`schemas/geojson.jaren.schema.json`](./schemas/geojson.jaren.schema.json) — the same grammar plus the two missing invariants, expressed with [`$query`](../validate).
|
|
467
|
+
|
|
468
|
+
The Jaren artifact is a mechanical *restriction* of the portable one — structurally identical, with a `$query` on the `polygon` and `multiPolygon` definitions — so the two cannot drift, and a consumer without `$query` support still gets the standard grammar. Ring closure is `$eq` of the first and last position; winding is the sign of the shoelace sum, computed with `$fold`:
|
|
469
|
+
|
|
470
|
+
```javascript
|
|
471
|
+
import { JarenValidator } from '@jarenjs/validate';
|
|
472
|
+
|
|
473
|
+
const unclosed = { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 1]]] };
|
|
474
|
+
new JarenValidator().compile(portableSchema)(unclosed); // true — structurally fine
|
|
475
|
+
new JarenValidator().compile(jarenSchema)(unclosed); // false — the ring never closes
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
The test suite pins six defect classes that slip past structural validation and are caught here, including one buried inside a `FeatureCollection`.
|
|
479
|
+
|
|
262
480
|
## Generating queries with LLMs
|
|
263
481
|
|
|
264
482
|
A query language whose entire grammar is one JSON Schema is a natural fit for **constrained decoding** — the structured-output mode of every major LLM API. Hand the schema to the provider and the model cannot emit an unknown operator, a three-argument `$eq`, or a mixed `$`/plain-key object; what remains is validated and compiled in two lines, and every failure carries a `docPath` you can feed back to the model for repair:
|
|
@@ -279,7 +497,11 @@ const query = compileJsonQuery(doc); // JsonQueryCompileError.docPath on
|
|
|
279
497
|
query(data); // JsonQueryRuntimeError.docPath at runtime
|
|
280
498
|
```
|
|
281
499
|
|
|
282
|
-
The validate step is not redundant: provider structured-output implementations support varying JSON Schema subsets regardless of the draft they declare (`patternProperties` and `propertyNames`, which the schema uses for binding names and map constructors, are not universally enforced). Validating locally catches whatever the provider's subset let through.
|
|
500
|
+
The validate step is not redundant: provider structured-output implementations support varying JSON Schema subsets regardless of the draft they declare (`patternProperties` and `propertyNames`, which the schema uses for binding names and map constructors, are not universally enforced, and strict modes often reject `oneOf`). Validating locally catches whatever the provider's subset let through.
|
|
501
|
+
|
|
502
|
+
For those strict subsets each grammar also ships an **LLM profile** — [`jaren-query.llm-profile.schema.json`](./schemas/jaren-query.llm-profile.schema.json) and [`jaren-jslt.llm-profile.schema.json`](./schemas/jaren-jslt.llm-profile.schema.json) — a mechanically derived, pure *relaxation* of the canonical artifact: `patternProperties`, `propertyNames` and asserted `format`s are removed (each restated in the nearest `description`, which the model still reads) and every `oneOf` becomes `anyOf`. That last swap costs no precision: the canonical grammar discriminates its branches by member name — a map constructor has no `$`-prefixed key, every phrase pins its own (`$const`, `$map`, `$call`, `$return`, …) or draws its single key from a disjoint operator enum — so at most one branch can ever match a document, and exactly-one and at-least-one accept the same language. It is also *required* once the name constraints are gone: `oneOf` would then reject `{"$eq": [1, 2]}` for matching two now-overlapping branches. Every canonical-valid document validates under the profile; the reverse is deliberately not guaranteed, which is exactly why the local validate-then-compile step above stays mandatory. Hand the profile to the provider's constrained decoder, validate locally against the canonical schema. (`@jarenjs/ai`'s `createStructuredOutput` packages this handshake.)
|
|
503
|
+
|
|
504
|
+
A relaxation, though, is not a *shrink*: restating every removed constraint in a `description` means the JSLT profile is 19,158 characters against the canonical 18,736, and on a small model an oversized `response_format` is the difference between a document and an empty reply. So the JSLT grammar also ships an **authoring profile** — [`jaren-jslt.authoring.schema.json`](./schemas/jaren-jslt.authoring.schema.json), **3,491 characters** — the same mechanical derivation with one more step: the `queryDocument` `$ref` that pulls the entire expression language into the document grammar is left *open*. What survives is the document shape (envelope, rules, match specs, modes); what leaves is the ~16 kB of phrase shapes. It is a pure widening of the canonical language, so nothing the engine can run becomes inexpressible, and it is deliberately weaker — a body of pure nonsense decodes under it. That is the trade, and it is only safe because the two authorities after the decoder are unchanged: validate against the canonical schema, then compile. The vocabulary the cut removes belongs in the prompt instead, where `@jarenjs/ai`'s `operatorCrib` puts it (operator names grouped by arity, ~1.1 kB, read off the query artifact). `@jarenjs/ai`'s [`createStylesheetAuthor`](../ai/README.md#when-the-grammar-is-too-big-to-decode-measured) packages the whole handshake.
|
|
283
505
|
|
|
284
506
|
Query documents are plain JSON, so they travel through the rest of an LLM toolchain as-is: function-call arguments, retrieval filters, audit logs, replay.
|
|
285
507
|
|
|
@@ -298,30 +520,31 @@ q(null, { doc: data }); // [ 'Sayings of the Century', 'Moby Dick' ]
|
|
|
298
520
|
|
|
299
521
|
`npm run benchmark:jsonquery` runs the scenario matrix against [fontoxpath](https://www.npmjs.com/package/fontoxpath) (a real XQuery 3.1 engine in JavaScript — the closest honest comparison) and [jsonata](https://www.npmjs.com/package/jsonata) (the popular practical alternative), asserting result equivalence on every document before timing anything. Each engine runs the same scenario written idiomatically in its own language (`benchmark/adaptors/jsonquery/`).
|
|
300
522
|
|
|
301
|
-
Measured with `npm run benchmark:jsonquery:profile` (2026-07-
|
|
523
|
+
Measured with `npm run benchmark:jsonquery:profile` (2026-07-27, Node v22.22.2; ratios are that engine's time over Jaren's):
|
|
302
524
|
|
|
303
525
|
| Scenario | Jaren | fontoxpath 3.34 | jsonata 2.2 |
|
|
304
526
|
|---|---|---|---|
|
|
305
527
|
| **4-book bookstore** | | | |
|
|
306
|
-
| singular access `$b.title` |
|
|
307
|
-
| filter + project (spec A.2) |
|
|
308
|
-
| join (spec A.3) |
|
|
309
|
-
| group + aggregate (spec A.4) | 4.
|
|
310
|
-
| deep reshape | 2.
|
|
528
|
+
| singular access `$b.title` | 451 ns (2.2M/s) | 12.0 µs (27x) | 7.3 µs (16x) |
|
|
529
|
+
| filter + project (spec A.2) | 3.6 µs (281k/s) | 60.6 µs (17x) | 26.2 µs (7.4x) |
|
|
530
|
+
| join (spec A.3) | 3.4 µs (291k/s) | 102.0 µs (30x) | 96.6 µs (28x) |
|
|
531
|
+
| group + aggregate (spec A.4) | 4.4 µs (229k/s) | n/a | 52.6 µs (12x) |
|
|
532
|
+
| deep reshape | 2.4 µs (426k/s) | 173.1 µs (74x) | 55.8 µs (24x) |
|
|
311
533
|
| **10,000-book bookstore** | | | |
|
|
312
|
-
| singular access |
|
|
313
|
-
| filter + project | 1.
|
|
314
|
-
| join (measured at 1,000 books) |
|
|
315
|
-
| group + aggregate | 2.
|
|
316
|
-
| deep reshape | 3.
|
|
317
|
-
| **compile, µs per query** |
|
|
534
|
+
| singular access | 324 ns (3.1M/s) | 9.0 µs (28x) | 4.6 µs (14x) |
|
|
535
|
+
| filter + project | 1.33 ms | 343.2 ms (259x) | 70.4 ms (53x) |
|
|
536
|
+
| join (measured at 1,000 books) | 331 µs | 1.64 s (4952x) | 1.71 s (5175x) |
|
|
537
|
+
| group + aggregate | 2.47 ms | n/a | 37.9 ms (15x) |
|
|
538
|
+
| deep reshape | 3.51 ms | 452.0 ms (129x) | 117.0 ms (33x) |
|
|
539
|
+
| **compile, µs per query** | 37.4 µs | 562.5 µs (15x) | 118.3 µs (3.2x) |
|
|
318
540
|
|
|
319
541
|
Honest caveats — what each competitor is optimized for:
|
|
320
542
|
|
|
321
543
|
- **fontoxpath** is an XML-first XPath/XQuery engine; JSON rides on XDM maps and arrays. The benchmark pre-converts each document to XDM *once, outside the timed loop* (per-call conversion would cost ~12 ms alone at 10k books), and fontoxpath has no public compile-only API, so its compile number is fresh-source evaluation minus cached re-evaluation. It does not implement `group by`. Its engineering effort goes into DOM navigation, buckets and XQuery Update — not JSON throughput.
|
|
322
544
|
- **jsonata** is a tree-walking interpreter whose `evaluate()` is async since 2.x; its numbers include that promise overhead because its API imposes it. It is optimized for expressiveness and embeddability, not raw speed.
|
|
323
545
|
- **Jaren**'s compile number includes `JSON.parse` of the query text, since the competitors parse text too.
|
|
324
|
-
- The join is a naive O(books × ratings) nested loop
|
|
546
|
+
- The join row is the one place these engines are not doing the same work, and the four-digit ratio says so rather than hiding it. fontoxpath and jsonata run it as a naive O(books × ratings) nested loop; Jaren's planner recognizes the uncorrelated equijoin and answers it from a hash table, which is O(books + ratings). At four books that is worth nothing (3.4 µs against 96–102 µs, the same 28–30x as every other small-document row); at 1,000 books it is the whole difference. Read the 4,952x as "different algorithm", not "faster engine" — and note the rewrite is declined whenever it would be observable (a `$as`, a `$let`, a correlated probe side), in which case Jaren runs the same nested loop they do. The row stays capped at 1,000 books because raising it would grow their cost quadratically and Jaren's linearly, measuring the cap instead of the engines.
|
|
547
|
+
- The 4-book `singular` cell reads slower than the 1,000-book one (451 ns vs 335 ns) run to run. That is JIT/IC noise across the cell sequence, not a real cost curve; pinning it down is a `--cell-order` shuffle on the roadmap.
|
|
325
548
|
|
|
326
549
|
## JSLT — declarative JSON transformation
|
|
327
550
|
|
|
@@ -400,6 +623,8 @@ const annotateBooks = compileJsltStylesheet([
|
|
|
400
623
|
], { compileTypeTest: createTypeTestCompiler() });
|
|
401
624
|
```
|
|
402
625
|
|
|
626
|
+
For render-loop consumers there is one more lever: `compileJsltStylesheet(doc, { memo: true })` memoizes rule outputs by (location, value reference). A compile-time analysis marks every rule whose output provably depends on nothing but the matched value (no `$root`/`$path`/user externals, transitively through `$apply`, and no root references inside match-path filters); those rules return their previous output **by reference** when the same subtree reference shows up at the same location. Across copy-on-write updates an unchanged document transforms in O(1) and a one-field change re-evaluates one spine — the fuel for the `@jarenjs/view` patcher's reference-equality fast path, which is why `@jarenjs/app` compiles every view with it. The cache is two-generational (entries unused for one transform retire), and memoized outputs must be treated as immutable.
|
|
627
|
+
|
|
403
628
|
The complete stylesheet grammar is published for validators and LLM constrained decoding as [`jaren-jslt.schema.json`](./schemas/jaren-jslt.schema.json) (draft 2020-12) and its mechanically derived [`jaren-jslt.draft-07.schema.json`](./schemas/jaren-jslt.draft-07.schema.json) twin. Rule-body definitions are mechanically copied from the query artifact and extended only with `$apply`, so the query vocabulary stays closed. Provider structured-output implementations still support uneven schema subsets; validate the generated document locally before compiling it, as described in [JSLT-FORMAT Appendix B](./docs/JSLT-FORMAT.md#appendix-b-llm-structured-output-non-normative).
|
|
404
629
|
|
|
405
630
|
### JSLT benchmark
|
|
@@ -464,8 +689,8 @@ Unmatched nodes follow the XSLT built-in template rules, restated for JSON: cont
|
|
|
464
689
|
|
|
465
690
|
## Roadmap
|
|
466
691
|
|
|
467
|
-
This package's roadmap lives in the repository-wide [ROADMAP](../../ROADMAP.md), under its `@jarenjs/json` sections:
|
|
692
|
+
This package's roadmap lives in the repository-wide [ROADMAP](../../docs/ROADMAP.md), under its `@jarenjs/json` sections: hoisting `$`-absolute comparables out of filter loops, first-class function values, the JSLT single-walk matcher, XQuery front-end `xs:*` casts, and more. Recently landed from that list: hash-joined equijoins and counting-loop `$range` iteration, the `$fold` accumulator clause, `$allowing-empty` and window bindings, the RFC 3339 date operators, closed-world compilation and the `steps`/`depth` execution limits.
|
|
468
693
|
|
|
469
694
|
## Development
|
|
470
695
|
|
|
471
|
-
Unit tests live in `test/json/` at the repository root (`npm run test:json`); the JSONPath tests are built from the RFC's own examples, the query and JSLT tests from their normative fixtures (each schema corpus validates against both artifact drafts), and every example in this README runs in `test/json/readme-examples.test.js`. This package's internals are described in its own [ARCHITECTURE](./ARCHITECTURE.md) document. Benchmarks (all documented in the [benchmark workspace README](../../benchmark/README.md)): `benchmark/jsonpath.js` (JSONPath compliance + performance), `benchmark/jsonpointer.js` (compiled pointers vs the interpretive resolver and the `jsonpointer` npm package), `benchmark/jsonquery.js` (query engine vs fontoxpath/jsonata), `benchmark/jslt.js` (stylesheet engine vs native JS/JSONata), `benchmark/qt3-runner.js` (W3C QT3 scorecard through the XQuery front-end). See the repository [README](../../README.md) and [ARCHITECTURE](../../ARCHITECTURE.md) for the monorepo picture.
|
|
696
|
+
Unit tests live in `test/json/` at the repository root (`npm run test:json`); the JSONPath tests are built from the RFC's own examples, the query and JSLT tests from their normative fixtures (each schema corpus validates against both artifact drafts), and every example in this README runs in `test/json/readme-examples.test.js`. This package's internals are described in its own [ARCHITECTURE](./ARCHITECTURE.md) document. Benchmarks (all documented in the [benchmark workspace README](../../benchmark/README.md)): `benchmark/jsonpath.js` (JSONPath compliance + performance), `benchmark/jsonpointer.js` (compiled pointers vs the interpretive resolver and the `jsonpointer` npm package), `benchmark/jsonquery.js` (query engine vs fontoxpath/jsonata), `benchmark/jslt.js` (stylesheet engine vs native JS/JSONata), `benchmark/qt3-runner.js` (W3C QT3 scorecard through the XQuery front-end). See the repository [README](../../README.md) and [ARCHITECTURE](../../docs/ARCHITECTURE.md) for the monorepo picture.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when a value cannot be canonicalized because it is not
|
|
3
|
+
* JSON data (RFC 8785 section 3.2.1). `dataPath` is an RFC 6901 JSON
|
|
4
|
+
* Pointer to the offending value, so a rejected document says which
|
|
5
|
+
* member was at fault rather than only that one was.
|
|
6
|
+
*
|
|
7
|
+
* Extends `TypeError`: passing a non-JSON value to a canonicalizer is a
|
|
8
|
+
* programming error, the same class `JSON.stringify` raises for a
|
|
9
|
+
* BigInt or a cycle.
|
|
10
|
+
*/
|
|
11
|
+
export declare class JsonCanonicalizeError extends TypeError {
|
|
12
|
+
dataPath: any;
|
|
13
|
+
constructor(message: any, dataPath: any);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Serialize a JSON value to its canonical form (RFC 8785 / JCS): object
|
|
17
|
+
* members sorted by the UTF-16 code units of their names, numbers in
|
|
18
|
+
* the ECMAScript `Number::toString` form, strings with the shortest
|
|
19
|
+
* JSON escapes and no escaping of non-ASCII, and no insignificant
|
|
20
|
+
* whitespace. Two structurally equal values always produce identical
|
|
21
|
+
* text, which is what makes the result safe to hash or sign.
|
|
22
|
+
*
|
|
23
|
+
* Unlike `JSON.stringify`, nothing is dropped or coerced: `undefined`,
|
|
24
|
+
* a function, a symbol, a BigInt, `NaN`, `Infinity`, an unpaired
|
|
25
|
+
* surrogate, a circular reference and a class instance are all errors,
|
|
26
|
+
* because a canonicalizer that quietly rewrote its input would produce
|
|
27
|
+
* a signature over a document nobody sent.
|
|
28
|
+
*
|
|
29
|
+
* @param {any} value - The JSON value to canonicalize
|
|
30
|
+
* @returns {string} The canonical JSON text
|
|
31
|
+
* @throws {JsonCanonicalizeError} When the value is not JSON data
|
|
32
|
+
* @example
|
|
33
|
+
* canonicalizeJson({ b: 1, a: [2, 3] }); // '{"a":[2,3],"b":1}'
|
|
34
|
+
* canonicalizeJson({ 'ä': 1, 'a': 2 }); // '{"a":2,"ä":1}'
|
|
35
|
+
* canonicalizeJson(1e21); // '1e+21'
|
|
36
|
+
*/
|
|
37
|
+
export declare function canonicalizeJson(value: any): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mutable state of one write application. `owned` is the set of
|
|
3
|
+
* nodes this application created and may mutate freely; `null` means
|
|
4
|
+
* in-place mode (every node is owned).
|
|
5
|
+
*/
|
|
6
|
+
export declare function makeState(root: any, owned: any): {
|
|
7
|
+
root: any;
|
|
8
|
+
owned: any;
|
|
9
|
+
};
|
|
10
|
+
/** Ensure the root is owned before the first write into it. */
|
|
11
|
+
export declare function ownedRoot(state: any): any;
|
|
12
|
+
/**
|
|
13
|
+
* Return an owned version of `child`, writing the clone back into the
|
|
14
|
+
* (already owned) parent slot when one is taken.
|
|
15
|
+
*/
|
|
16
|
+
export declare function ownedChild(state: any, parent: any, child: any, key: any): any;
|
|
17
|
+
/**
|
|
18
|
+
* The concrete array index a step addresses in `parent`, or -1 when the
|
|
19
|
+
* step cannot address an array element. Typed index steps resolve
|
|
20
|
+
* negative indexes from the end; the RFC 6901 form never does.
|
|
21
|
+
*/
|
|
22
|
+
export declare function stepArrayIndex(parent: any, name: any, index: any): any;
|
|
23
|
+
/**
|
|
24
|
+
* Read the location `steps[0..len)` in `root` without cloning anything.
|
|
25
|
+
* Returns the shared NOTHING-style `miss` sentinel value passed in when
|
|
26
|
+
* the location does not exist (callers pick their own sentinel).
|
|
27
|
+
*/
|
|
28
|
+
export declare function readSteps(root: any, names: any, indexes: any, len: any, miss: any): any;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CodedError } from '@jarenjs/core/errors';
|
|
2
|
+
/**
|
|
3
|
+
* Base for errors that carry a stable `code` and a `docPath`, an RFC
|
|
4
|
+
* 6901 JSON Pointer into the offending document. A wrapped error is
|
|
5
|
+
* exposed through the native `cause` option when one is given. This is
|
|
6
|
+
* the positional adapter over `@jarenjs/core`'s {@link CodedError}: a
|
|
7
|
+
* trailing `undefined` cause means "no cause" (no own property), per
|
|
8
|
+
* the base's contract.
|
|
9
|
+
*/
|
|
10
|
+
export declare class CodedDocPathError extends CodedError {
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} name - The public class name for `error.name`
|
|
13
|
+
* @param {string} code - Stable diagnosis code
|
|
14
|
+
* @param {string} reason - What is wrong (bare; the message is composed)
|
|
15
|
+
* @param {string} docPath - JSON Pointer into the offending document
|
|
16
|
+
* @param {unknown} [cause] - Wrapped error, when there is one
|
|
17
|
+
*/
|
|
18
|
+
constructor(name: string, code: string, reason: string, docPath: string, cause?: unknown);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Base for syntax errors over a source string: `source` and `position`
|
|
22
|
+
* locate the offending token, and the message is prefixed with a
|
|
23
|
+
* human-readable language label. Deliberately NOT a {@link CodedError}:
|
|
24
|
+
* this family carries no stable code and locates by source offset, not
|
|
25
|
+
* by document pointer.
|
|
26
|
+
*/
|
|
27
|
+
export declare class LabeledSyntaxError extends SyntaxError {
|
|
28
|
+
source: string;
|
|
29
|
+
position: number;
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} name - The public class name for `error.name`
|
|
32
|
+
* @param {string} label - Language label for the message prefix
|
|
33
|
+
* @param {string} message - What is wrong
|
|
34
|
+
* @param {string} source - The offending source text
|
|
35
|
+
* @param {number} position - 0-based index of the offending token
|
|
36
|
+
*/
|
|
37
|
+
constructor(name: string, label: string, message: string, source: string, position: number);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build a `fail(code, message, docPath)` throw-adapter for a coded
|
|
41
|
+
* error class; compilers use it to keep rejection one-liners readable.
|
|
42
|
+
* @param {new (code: string, message: string, docPath: string) => Error} ErrorClass
|
|
43
|
+
* @returns {(code: string, message: string, docPath: string) => never}
|
|
44
|
+
*/
|
|
45
|
+
export declare function failerFor(ErrorClass: new (code: string, message: string, docPath: string) => Error): (code: string, message: string, docPath: string) => never;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
|
+
import { CodedError } from '@jarenjs/core/errors';
|
|
2
|
+
import { CodedDocPathError } from '../errors.js';
|
|
1
3
|
/**
|
|
2
4
|
* Error thrown when a JSLT stylesheet is rejected at compile time
|
|
3
5
|
* (`JT0xxx` codes).
|
|
4
6
|
*/
|
|
5
|
-
export declare class JsltCompileError extends
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
export declare class JsltCompileError extends CodedError {
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} code
|
|
10
|
+
* @param {string} reason
|
|
11
|
+
* @param {string} docPath
|
|
12
|
+
* @param {...unknown} cause - When a fourth argument is passed AT
|
|
13
|
+
* ALL, it is retained as an own `cause` — even `undefined`, so a
|
|
14
|
+
* host hook that threw `undefined` stays distinguishable from "no
|
|
15
|
+
* cause".
|
|
16
|
+
*/
|
|
17
|
+
constructor(code: string, reason: string, docPath: string, ...cause: unknown[]);
|
|
9
18
|
}
|
|
10
19
|
/**
|
|
11
20
|
* Error thrown when evaluating a compiled JSLT stylesheet fails
|
|
12
21
|
* (`JT2xxx` codes).
|
|
13
22
|
*/
|
|
14
|
-
export declare class JsltRuntimeError extends
|
|
15
|
-
code: any;
|
|
16
|
-
docPath: any;
|
|
17
|
-
constructor(code: any, message: any, docPath: any, cause?: undefined);
|
|
23
|
+
export declare class JsltRuntimeError extends CodedDocPathError {
|
|
24
|
+
constructor(code: any, reason: any, docPath: any, cause?: undefined);
|
|
18
25
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { JsltCompileError, JsltRuntimeError } from './errors.js';
|
|
2
|
+
export { createJsltRegistry } from './registry.js';
|
|
3
|
+
export { mathPack, financePack, statsPack, allPacks } from './packs/index.js';
|
|
2
4
|
/**
|
|
3
5
|
* Compile a Jaren JSLT 0.1 stylesheet into a reusable transformation.
|
|
4
6
|
*
|
|
@@ -17,6 +19,20 @@ export { JsltCompileError, JsltRuntimeError } from './errors.js';
|
|
|
17
19
|
* [options.compileTypeTest] - validator-agnostic hook compiling schema
|
|
18
20
|
* match conditions and schema literals inside query bodies
|
|
19
21
|
* @param {number} [options.maxDepth=1024] - maximum dispatch nesting depth
|
|
22
|
+
* @param {boolean} [options.memo=false] - memoize rule outputs by
|
|
23
|
+
* (location, value reference): across repeated transforms of
|
|
24
|
+
* copy-on-write-updated documents, unchanged subtrees return the
|
|
25
|
+
* PREVIOUS output by reference — the fuel for reference-equality
|
|
26
|
+
* fast paths downstream (the @jarenjs/view patcher). Only rules whose
|
|
27
|
+
* output provably depends on nothing but the matched value are cached
|
|
28
|
+
* (no $root/$path/user externals, transitively through $apply, and no
|
|
29
|
+
* root references inside match-path filters); everything else runs
|
|
30
|
+
* normally. Memoized outputs MUST be treated as immutable, and the
|
|
31
|
+
* cache retains the previous transform's outputs (two generations).
|
|
32
|
+
* @param {Record<string, import('../path.js').JSONPathFunction>}
|
|
33
|
+
* [options.pathFunctions] - custom JSONPath function extensions (RFC
|
|
34
|
+
* 9535 section 2.4), available in rule match paths and in the path
|
|
35
|
+
* strings inside rule bodies
|
|
20
36
|
* @returns {function} reusable `transform(data, externals?)` function
|
|
21
37
|
* @throws {import('./errors.js').JsltCompileError} when compilation fails
|
|
22
38
|
* @example
|
|
@@ -29,6 +45,7 @@ export { JsltCompileError, JsltRuntimeError } from './errors.js';
|
|
|
29
45
|
export declare function compileJsltStylesheet(doc: any, options?: {
|
|
30
46
|
compileTypeTest?: (schemaJson: any, docPath: string) => ((value: any) => boolean);
|
|
31
47
|
maxDepth?: number;
|
|
48
|
+
memo?: boolean;
|
|
32
49
|
}): Function;
|
|
33
50
|
/**
|
|
34
51
|
* Transform a JSON value with a JSLT stylesheet in one call.
|
|
@@ -41,6 +58,9 @@ export declare function compileJsltStylesheet(doc: any, options?: {
|
|
|
41
58
|
* @param {(schemaJson: any, docPath: string) => ((value: any) => boolean)}
|
|
42
59
|
* [options.compileTypeTest] - schema type-test compiler
|
|
43
60
|
* @param {number} [options.maxDepth=1024] - maximum dispatch nesting depth
|
|
61
|
+
* @param {boolean} [options.memo=false] - memoize rule outputs
|
|
62
|
+
* @param {Record<string, import('../path.js').JSONPathFunction>}
|
|
63
|
+
* [options.pathFunctions] - custom JSONPath function extensions
|
|
44
64
|
* @returns {any} `undefined`, one JSON item, or an array of result items
|
|
45
65
|
* @throws {import('./errors.js').JsltCompileError} when compilation fails
|
|
46
66
|
* @throws {import('./errors.js').JsltRuntimeError} when dispatch fails
|
|
@@ -50,4 +70,6 @@ export declare function compileJsltStylesheet(doc: any, options?: {
|
|
|
50
70
|
export declare function transformJson(stylesheet: any, data: any, externals?: object, options?: {
|
|
51
71
|
compileTypeTest?: (schemaJson: any, docPath: string) => ((value: any) => boolean);
|
|
52
72
|
maxDepth?: number;
|
|
73
|
+
memo?: boolean;
|
|
74
|
+
pathFunctions?: Record<string, import('../path.js').JSONPathFunction>;
|
|
53
75
|
}): any;
|