@jarenjs/json 0.9.2
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 +175 -0
- package/LICENSE +21 -0
- package/README.md +471 -0
- package/dist/types/basic.d.ts +32 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/jslt/dispatch.d.ts +11 -0
- package/dist/types/jslt/errors.d.ts +18 -0
- package/dist/types/jslt/index.d.ts +53 -0
- package/dist/types/jslt/stylesheet.d.ts +8 -0
- package/dist/types/jtlt/desugar.d.ts +19 -0
- package/dist/types/jtlt/errors.d.ts +18 -0
- package/dist/types/jtlt/index.d.ts +57 -0
- package/dist/types/jtlt/template.d.ts +8 -0
- package/dist/types/jtlt/writer.d.ts +6 -0
- package/dist/types/path.d.ts +235 -0
- package/dist/types/pointer.d.ts +114 -0
- package/dist/types/query/compile.d.ts +21 -0
- package/dist/types/query/errors.d.ts +18 -0
- package/dist/types/query/index.d.ts +70 -0
- package/dist/types/query/normalize.d.ts +68 -0
- package/dist/types/query/operators.d.ts +424 -0
- package/dist/types/query/runtime.d.ts +93 -0
- package/dist/types/segments.d.ts +62 -0
- package/dist/types/xquery/index.d.ts +19 -0
- package/dist/types/xquery/parse.d.ts +20 -0
- package/docs/JSLT-FORMAT.md +861 -0
- package/docs/JSLT-PRELUDE.md +159 -0
- package/docs/JTLT-FORMAT.md +659 -0
- package/docs/QUERY-FORMAT.md +1221 -0
- package/docs/XQUERY-FRONTEND.md +321 -0
- package/package.json +81 -0
- package/schemas/jaren-jslt.draft-07.schema.json +776 -0
- package/schemas/jaren-jslt.schema.json +776 -0
- package/schemas/jaren-query.draft-07.schema.json +613 -0
- package/schemas/jaren-query.schema.json +375 -0
- package/src/basic.js +300 -0
- package/src/index.js +4 -0
- package/src/jslt/dispatch.js +934 -0
- package/src/jslt/errors.js +34 -0
- package/src/jslt/index.js +121 -0
- package/src/jslt/stylesheet.js +234 -0
- package/src/jtlt/desugar.js +231 -0
- package/src/jtlt/errors.js +34 -0
- package/src/jtlt/index.js +155 -0
- package/src/jtlt/template.js +130 -0
- package/src/jtlt/writer.js +110 -0
- package/src/path.js +977 -0
- package/src/pointer.js +453 -0
- package/src/query/compile.js +817 -0
- package/src/query/errors.js +33 -0
- package/src/query/index.js +150 -0
- package/src/query/normalize.js +1047 -0
- package/src/query/operators.js +1253 -0
- package/src/query/runtime.js +233 -0
- package/src/segments.js +627 -0
- package/src/xquery/index.js +35 -0
- package/src/xquery/parse.js +1647 -0
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
# The Jaren JSLT Format
|
|
2
|
+
|
|
3
|
+
**Version 0.1 — Specification**
|
|
4
|
+
|
|
5
|
+
Module: `@jarenjs/json/jslt`. This document is the language contract for the
|
|
6
|
+
Jaren JSLT template layer, the way [QUERY-FORMAT](./QUERY-FORMAT.md) is the
|
|
7
|
+
contract for the query engine it builds on. The design history lives in
|
|
8
|
+
[JSLT-PRELUDE](./JSLT-PRELUDE.md), which this specification supersedes.
|
|
9
|
+
|
|
10
|
+
> **Naming note (non-normative).** "JSLT" here names the Jaren template
|
|
11
|
+
> layer — *JSON stylesheet language for transformations*, the XSLT
|
|
12
|
+
> derivative of this stack. It is unrelated to Schibsted's JSLT language;
|
|
13
|
+
> final naming/branding is an open question and is deliberately not settled
|
|
14
|
+
> here, mirroring the query format's naming note.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Introduction
|
|
19
|
+
|
|
20
|
+
### 1.1 What this language is
|
|
21
|
+
|
|
22
|
+
A JSLT **stylesheet** is a JSON document holding an ordered list of
|
|
23
|
+
**template rules**. Each rule says "when you meet a value shaped like this,
|
|
24
|
+
produce that", and a recursive dispatch engine — XSLT's `apply-templates`
|
|
25
|
+
idea — does the walking. XSLT's enduring pain is that "shaped like this"
|
|
26
|
+
(XPath patterns), "typed like this" (XML Schema), and the output vocabulary
|
|
27
|
+
are three disjoint languages bolted together. Here all three are
|
|
28
|
+
vocabularies this stack already compiles:
|
|
29
|
+
|
|
30
|
+
- **matching** — RFC 9535 JSONPath, already compiled by this package (§3);
|
|
31
|
+
- **typing** — JSON Schema, embedded through the same validator-agnostic
|
|
32
|
+
`compileTypeTest` hook the query engine uses (§9): a schema *is* a
|
|
33
|
+
pattern, with no schema-aware processor ceremony;
|
|
34
|
+
- **producing** — Jaren JSON Query documents (QUERY-FORMAT.md), whose
|
|
35
|
+
documents are JSON values the way XSLT stylesheets are XML documents.
|
|
36
|
+
|
|
37
|
+
The architecture pillars, stated once (non-normative but binding on the
|
|
38
|
+
reference implementation): **one stack, zero new dependencies**. JSLT adds
|
|
39
|
+
exactly **one operator** (`$apply`, §6) and a thin compiled dispatch
|
|
40
|
+
runtime. The layer lives at `packages/json/src/jslt/` — a module boundary
|
|
41
|
+
inside `@jarenjs/json`, like `xquery/` — NOT a new package: it needs the
|
|
42
|
+
query engine's internals (`normalizeQuery`, `compileNode`, the sequence
|
|
43
|
+
runtime), and a separate package would force those onto the public surface.
|
|
44
|
+
The standalone `@jarenjs/jslt` package sketched in the prelude (§7 there)
|
|
45
|
+
stays a roadmap note.
|
|
46
|
+
|
|
47
|
+
### 1.2 Conformance and normative language
|
|
48
|
+
|
|
49
|
+
The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL
|
|
50
|
+
NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and
|
|
51
|
+
**OPTIONAL** in this document are to be interpreted as described in
|
|
52
|
+
RFC 2119.
|
|
53
|
+
|
|
54
|
+
- A **producer** emits stylesheet documents and MUST emit documents valid
|
|
55
|
+
per this specification.
|
|
56
|
+
- A **consumer** (stylesheet compiler + transformation engine) MUST accept
|
|
57
|
+
every valid stylesheet, MUST reject invalid ones with the compile errors
|
|
58
|
+
of §10, and MUST raise the runtime errors of §10 under the conditions
|
|
59
|
+
specified there.
|
|
60
|
+
|
|
61
|
+
Everything QUERY-FORMAT.md specifies for query documents applies unchanged
|
|
62
|
+
to rule bodies except where this document says otherwise; the only body
|
|
63
|
+
extension is `$apply` (§6).
|
|
64
|
+
|
|
65
|
+
### 1.3 Terminology
|
|
66
|
+
|
|
67
|
+
- **Stylesheet** — the top-level JSON document handed to the compiler (§2).
|
|
68
|
+
- **Rule** — one template rule object (§2.2).
|
|
69
|
+
- **Body** — a rule's output expression: a Jaren JSON Query document
|
|
70
|
+
extended with `$apply`.
|
|
71
|
+
- **Dispatch** — finding and firing the winning rule for one value (§5).
|
|
72
|
+
- **Mode** — a named partition of the rule set (§7).
|
|
73
|
+
- **Location** — the normalized path (RFC 9535 §2.7) of a value inside the
|
|
74
|
+
input document, when it has one (§3.2, §6).
|
|
75
|
+
- **Disposition** — a mode's built-in behavior for unmatched values (§5).
|
|
76
|
+
|
|
77
|
+
### 1.4 Changes from the prelude
|
|
78
|
+
|
|
79
|
+
JSLT-PRELUDE.md declared its spellings provisional; these are the final
|
|
80
|
+
calls, listed here so readers of the prelude are not misled:
|
|
81
|
+
|
|
82
|
+
1. **`$schema-match` is gone.** A schema match is the `"schema"` member of
|
|
83
|
+
the `match` object (§3.1); position and shape conditions are two members
|
|
84
|
+
of one object, not two wrapper vocabularies.
|
|
85
|
+
2. **The stylesheet envelope.** Besides the prelude's bare rule array, an
|
|
86
|
+
object form `{"$jslt": "0.1", "rules": [...]}` carries the version and
|
|
87
|
+
the stylesheet-level options `unmatched` and `modes` (§2.1).
|
|
88
|
+
3. **Dispositions are `share` / `fresh` / `error`** under the `unmatched`
|
|
89
|
+
member (§5), replacing the prelude's deep-copy/shallow-copy pair. There
|
|
90
|
+
is **no XSLT-style deep-copy default** — see §5.2 for the rationale;
|
|
91
|
+
XSLT's literal deep-copy remains expressible per rule as
|
|
92
|
+
`{"match": m, "body": "$"}`.
|
|
93
|
+
4. **Conflict resolution is fixed and small** (§4): explicit `priority`,
|
|
94
|
+
then three default priorities, then document order with later rules
|
|
95
|
+
winning. The prelude's schema-specificity metric (const > enum > type…)
|
|
96
|
+
is explicitly a non-goal of 0.1 and stays on the roadmap.
|
|
97
|
+
5. **The `[]` idiom.** The prelude's §2 sketch wrote
|
|
98
|
+
`"children": {"$apply": "$.chapters[*]"}` — that bare member form is a
|
|
99
|
+
runtime error (JQ2001) as soon as a node has two or more chapters. The
|
|
100
|
+
correct idiom is `"children": [{"$apply": "$.chapters[*]"}]`, with the
|
|
101
|
+
array-constructor brackets (§6.3).
|
|
102
|
+
6. **`$apply`'s mode argument** is spelled as the argument-list form
|
|
103
|
+
`{"$apply": [selector, mode]}` (§6.1), and an omitted mode defaults to
|
|
104
|
+
the **rule's own mode**, not XSLT 1.0's unnamed mode (§6.5).
|
|
105
|
+
7. **Vocabulary placement is decided**: `$apply` lives in the `jslt`
|
|
106
|
+
module, injected per rule body by the stylesheet compiler. The core
|
|
107
|
+
query vocabulary and the published query-format schema are unchanged
|
|
108
|
+
(§6.1).
|
|
109
|
+
8. **Reserved parameters.** `root` and `path` are engine-bound externals
|
|
110
|
+
available in every body (§8.2); the prelude had no parameter story.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 2. The stylesheet document
|
|
115
|
+
|
|
116
|
+
### 2.1 Top level
|
|
117
|
+
|
|
118
|
+
A stylesheet is either:
|
|
119
|
+
|
|
120
|
+
1. a JSON **array of rules** (the shorthand form) — implies version `"0.1"`
|
|
121
|
+
and all defaults; or
|
|
122
|
+
2. the **envelope object**:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{ "$jslt": "0.1",
|
|
126
|
+
"rules": [],
|
|
127
|
+
"unmatched": "share",
|
|
128
|
+
"modes": { "toc": { "unmatched": "error" } } }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`$jslt` and `rules` are REQUIRED (`rules` MUST be an array of rules);
|
|
132
|
+
`unmatched` and `modes` are OPTIONAL. Unknown envelope members are
|
|
133
|
+
compile error JT0001. A `$jslt` value other than the string `"0.1"` —
|
|
134
|
+
including non-string values — is compile error JT0004.
|
|
135
|
+
|
|
136
|
+
A stylesheet that is neither an array nor an object of the envelope shape
|
|
137
|
+
is JT0001.
|
|
138
|
+
|
|
139
|
+
- `"unmatched"`: one of `"share"` (the default), `"fresh"`, or `"error"` —
|
|
140
|
+
the built-in-rule disposition of §5; any other value is JT0001. The
|
|
141
|
+
envelope's `unmatched` sets the default for every mode.
|
|
142
|
+
- `"modes"`: an object `{ modeName: { "unmatched": disposition } }` of
|
|
143
|
+
per-mode overrides. Unknown members inside a mode object are JT0001.
|
|
144
|
+
Modes need NOT be declared to be used (§7); declaring one only overrides
|
|
145
|
+
its disposition.
|
|
146
|
+
|
|
147
|
+
### 2.2 Rules
|
|
148
|
+
|
|
149
|
+
A **rule** is an object with the members
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{ "match": "$..price", "mode": "render", "priority": 2, "body": "$" }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
- `body` (REQUIRED) — a Jaren JSON Query document per QUERY-FORMAT.md,
|
|
156
|
+
extended with the `$apply` operator (§6). Everything the query format
|
|
157
|
+
specifies — encoding rules, FLWOR, operators, schema operators, errors —
|
|
158
|
+
applies inside a body verbatim.
|
|
159
|
+
- `match` (OPTIONAL) — what the rule fires on (§3). A rule with no `match`
|
|
160
|
+
member matches every value (an **unconditional rule**).
|
|
161
|
+
- `mode` (OPTIONAL) — a string naming the rule's mode (§7); default is the
|
|
162
|
+
unnamed mode `""`. A non-string `mode` is JT0002.
|
|
163
|
+
- `priority` (OPTIONAL) — any JSON number, for explicit conflict resolution
|
|
164
|
+
(§4). A non-number `priority` is JT0002.
|
|
165
|
+
|
|
166
|
+
A rule that is not an object, or that lacks `body`, is JT0002. Unknown rule
|
|
167
|
+
members are JT0002 — the vocabulary is **closed**, the same culture as the
|
|
168
|
+
query format: no silent annotations.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 3. Matching
|
|
173
|
+
|
|
174
|
+
### 3.1 The `match` member
|
|
175
|
+
|
|
176
|
+
`match` is either an RFC 9535 JSONPath query string, or an object with **at
|
|
177
|
+
least one** of the members `"path"` and `"schema"` and **no other members**
|
|
178
|
+
(violations are JT0003):
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{ "match": { "path": "$.store.book[*]",
|
|
182
|
+
"schema": { "type": "object", "required": ["isbn"] } } }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- A bare string is shorthand for `{"path": string}`.
|
|
186
|
+
- `"path"` MUST be a valid RFC 9535 query string; a syntactically invalid
|
|
187
|
+
path is JT0003 (with the `JSONPathSyntaxError` as `cause`).
|
|
188
|
+
- `"schema"` is a **JSON Schema literal**, taken verbatim per the
|
|
189
|
+
schema-literal rules of QUERY-FORMAT §8.11: never evaluated as a query
|
|
190
|
+
expression, deep-copied and frozen, compiled exactly once at stylesheet
|
|
191
|
+
compile time by the `compileTypeTest` hook (§9).
|
|
192
|
+
- An empty object `{}` is JT0003 (it would match nothing meaningfully and
|
|
193
|
+
is always a mistake; write no `match` member for an unconditional rule).
|
|
194
|
+
|
|
195
|
+
When both `path` and `schema` are present, **both** conditions MUST hold
|
|
196
|
+
for the rule to match.
|
|
197
|
+
|
|
198
|
+
### 3.2 Path matching is positional
|
|
199
|
+
|
|
200
|
+
A value matches a `"path"` condition **iff it sits at a location the path
|
|
201
|
+
selects from the input document root**. The path is a plain RFC 9535 query
|
|
202
|
+
over the input document: filters inside it see `$` as the **input root**
|
|
203
|
+
(not the candidate value), exactly as in any absolute path.
|
|
204
|
+
|
|
205
|
+
Normative consequence: values that have **no location** — values computed
|
|
206
|
+
by a rule body, or selected by a non-path `$apply` selector (§6.4) — can
|
|
207
|
+
NEVER match a path condition. Only `schema` conditions and unconditional
|
|
208
|
+
rules apply to them.
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{ "match": "$..price", "body": { "$mul": ["$", 1.21] } }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
matches every value sitting at a `price` member anywhere in the input — and
|
|
215
|
+
does not match the number `10` produced by another rule's body, however
|
|
216
|
+
price-like it looks.
|
|
217
|
+
|
|
218
|
+
### 3.3 Schema matching is shape-based
|
|
219
|
+
|
|
220
|
+
A value matches a `"schema"` condition **iff it satisfies the schema**,
|
|
221
|
+
tested by the compiled predicate the `compileTypeTest` hook returned — the
|
|
222
|
+
same hook, contract, and per-item semantics as QUERY-FORMAT §8.11. Shape
|
|
223
|
+
matching needs no location: it applies to every dispatched value, wherever
|
|
224
|
+
it came from.
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{ "match": { "schema": { "type": "object", "required": ["isbn"] } },
|
|
228
|
+
"body": { "title": "$.title" } }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### 3.4 Implementation note (non-normative)
|
|
232
|
+
|
|
233
|
+
The intended mechanism constrains the semantics, so it is recorded here:
|
|
234
|
+
the consumer evaluates all match paths against the input **once per
|
|
235
|
+
transformation** (lazily per mode), yielding the set of matched locations;
|
|
236
|
+
dispatching a located value tests set membership plus the schema
|
|
237
|
+
predicates. This is sound because documents are **immutable during a
|
|
238
|
+
transform**: rule bodies construct fresh values per the query engine's
|
|
239
|
+
constructor rules and never mutate the input, so no body ever observes
|
|
240
|
+
partial pre-pass state.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 4. Conflict resolution
|
|
245
|
+
|
|
246
|
+
The whole algorithm:
|
|
247
|
+
|
|
248
|
+
1. **Rank** = (`priority`, document order).
|
|
249
|
+
2. `priority` is any JSON number. When absent, the **default priority** is:
|
|
250
|
+
|
|
251
|
+
| rule | default priority |
|
|
252
|
+
|---|---|
|
|
253
|
+
| both `path` and `schema` present | `1` |
|
|
254
|
+
| exactly one of `path` / `schema` | `0` |
|
|
255
|
+
| unconditional (no `match`) | `-1` |
|
|
256
|
+
|
|
257
|
+
3. Higher priority wins. Ties break by document order with **later rules
|
|
258
|
+
winning** — user rules appended after a library's override it.
|
|
259
|
+
4. The first match in rank order **fires**; there are no ambiguity errors
|
|
260
|
+
and no warnings.
|
|
261
|
+
|
|
262
|
+
Deep schema-specificity metrics (a `const` beating an `enum` beating a
|
|
263
|
+
`type`, …) are explicitly a **non-goal of 0.1**; they stay on the roadmap.
|
|
264
|
+
XSLT's default-priority table is the cautionary tale this section is
|
|
265
|
+
designed to beat by being small.
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
[ { "match": "$..price", "body": "$" },
|
|
269
|
+
{ "match": "$..price", "body": { "$mul": ["$", 1.21] } } ]
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Both rules tie at priority `0`; the later one wins and every price is
|
|
273
|
+
multiplied.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## 5. Dispatch and the built-in rule
|
|
278
|
+
|
|
279
|
+
### 5.1 Processing model
|
|
280
|
+
|
|
281
|
+
A **transformation** is the dispatch of the input document root in the
|
|
282
|
+
unnamed mode. **Dispatching** a value in a mode:
|
|
283
|
+
|
|
284
|
+
1. Find the highest-ranking rule of the mode that matches the value (§3,
|
|
285
|
+
§4). If one fires, the dispatch result is its **body's result**, with
|
|
286
|
+
the matched value as the body's query input `$` (no context-item drift,
|
|
287
|
+
per the query format).
|
|
288
|
+
2. If no rule fires, the **built-in rule** applies, per the mode's
|
|
289
|
+
`unmatched` disposition:
|
|
290
|
+
|
|
291
|
+
- **`"share"` (the default).** A scalar (string, number, boolean, `null`)
|
|
292
|
+
is returned **as-is**. A container is rebuilt member-by-member (object)
|
|
293
|
+
or element-by-element (array), **dispatching every child**; the rebuilt
|
|
294
|
+
container follows the query format's constructor rules: an object member
|
|
295
|
+
whose child dispatch is empty is **omitted**; a child dispatch of two or
|
|
296
|
+
more items for an object member is runtime error JT2002; array children
|
|
297
|
+
splice their result sequences flat. **When every child comes back
|
|
298
|
+
identical (`===`) and complete, the original container itself is
|
|
299
|
+
returned** — the output shares unmatched subtrees with the input. The
|
|
300
|
+
empty stylesheet is the identity transform, `===` included.
|
|
301
|
+
- **`"fresh"`.** The same semantics, but the built-in rule **always
|
|
302
|
+
returns the freshly rebuilt container**, never the original — callers
|
|
303
|
+
get a tree they may mutate without touching the input (the
|
|
304
|
+
forms/view-model case). Stated plainly: `fresh` governs **only built-in
|
|
305
|
+
rebuilds**; rule-body outputs are whatever the bodies return, and path
|
|
306
|
+
results inside bodies still share input subtrees (QUERY-FORMAT
|
|
307
|
+
behavior).
|
|
308
|
+
- **`"error"`.** An unmatched value is runtime error JT2003, naming the
|
|
309
|
+
value's location when it has one — the exhaustive-dispatch style.
|
|
310
|
+
|
|
311
|
+
### 5.2 No deep-copy default (divergence from the prelude)
|
|
312
|
+
|
|
313
|
+
JSLT has **no XSLT deep-copy default**, deliberately. Under XSLT 3.0
|
|
314
|
+
`on-no-match="deep-copy"` semantics, an unmatched root freezes the whole
|
|
315
|
+
document: a stylesheet whose only rule matches `$..price` would never fire
|
|
316
|
+
it, because the root is unmatched and the deep copy stops all further
|
|
317
|
+
matching. That contradicts the surgical-override promise that motivates
|
|
318
|
+
template engines — rules must apply at every depth.
|
|
319
|
+
|
|
320
|
+
In JSLT, rules conceptually apply to **every value**; the `unmatched`
|
|
321
|
+
disposition only chooses what happens **between** matches. XSLT's literal
|
|
322
|
+
deep-copy — copy this subtree, stop matching inside it — remains
|
|
323
|
+
expressible per rule:
|
|
324
|
+
|
|
325
|
+
```json
|
|
326
|
+
{ "match": "$.assets", "body": "$" }
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### 5.3 Sharing is normative
|
|
330
|
+
|
|
331
|
+
In `share` mode — and in rule bodies generally, since path results share
|
|
332
|
+
input subtrees per QUERY-FORMAT — **the output may alias input objects and
|
|
333
|
+
arrays; mutating the output mutates the input.** Consumers MUST implement
|
|
334
|
+
the `===` sharing of §5.1, and callers who intend to mutate the result MUST
|
|
335
|
+
use `fresh` (or copy). This is documented loudly on purpose: it is the
|
|
336
|
+
number-one operational surprise of value-sharing template engines.
|
|
337
|
+
|
|
338
|
+
### 5.4 Recursion and the depth guard
|
|
339
|
+
|
|
340
|
+
Recursion happens ONLY through `$apply` (§6) and the built-in rule's child
|
|
341
|
+
dispatches. A consumer MUST enforce a dispatch depth guard: the compile
|
|
342
|
+
option `maxDepth` (default `1024`) bounds the dispatch nesting depth, and
|
|
343
|
+
exceeding it is runtime error JT2001 — the self-application loop
|
|
344
|
+
|
|
345
|
+
```json
|
|
346
|
+
{ "rules": [ { "body": { "$apply": ["$"] } } ], "$jslt": "0.1" }
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
MUST die with JT2001, not a stack overflow.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## 6. `$apply` — the apply-templates operator
|
|
354
|
+
|
|
355
|
+
### 6.1 Availability
|
|
356
|
+
|
|
357
|
+
`$apply` is available ONLY inside rule bodies: the stylesheet compiler
|
|
358
|
+
injects it into each body compile. The query format's vocabulary is
|
|
359
|
+
**unchanged** and its published schema untouched — plain `compileJsonQuery`
|
|
360
|
+
documents still reject `$apply` as JQ0002.
|
|
361
|
+
|
|
362
|
+
### 6.2 Value forms
|
|
363
|
+
|
|
364
|
+
Following the `$orderby` disambiguation precedent of the query format
|
|
365
|
+
(QUERY-FORMAT §6.6, "an array value is always a list"):
|
|
366
|
+
|
|
367
|
+
- A **non-array** value is the selector expression; dispatch happens in the
|
|
368
|
+
**rule's own mode**:
|
|
369
|
+
|
|
370
|
+
```json
|
|
371
|
+
{ "$apply": "$.chapters[*]" }
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
- An **array** value is ALWAYS the argument-list form `[selector]` or
|
|
375
|
+
`[selector, mode]`, where `mode` is a **literal JSON string** naming the
|
|
376
|
+
target mode (`""` names the unnamed mode). The mode is not an
|
|
377
|
+
expression — dynamic mode selection is a non-goal of 0.1:
|
|
378
|
+
|
|
379
|
+
```json
|
|
380
|
+
{ "$apply": ["$.sections[*]", "toc"] }
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
- To apply templates to a **constructed array**, write the selector
|
|
384
|
+
explicitly: `{"$apply": [{"$const": [1, 2]}]}` or
|
|
385
|
+
`{"$apply": [{"$seq": [1, 2]}]}` — a bare array value never means "apply
|
|
386
|
+
to this array".
|
|
387
|
+
|
|
388
|
+
Wrong shapes — an empty or overlong argument list, a non-string mode — are
|
|
389
|
+
JQ0003 inside the body, surfacing as JT0007 at the stylesheet level (§10).
|
|
390
|
+
|
|
391
|
+
### 6.3 Semantics
|
|
392
|
+
|
|
393
|
+
Evaluate the selector against the current input `$`; for each item of the
|
|
394
|
+
result, **in order**, dispatch it (§5) in the target mode; concatenate the
|
|
395
|
+
dispatch results into one sequence. An empty selector result is the empty
|
|
396
|
+
sequence.
|
|
397
|
+
|
|
398
|
+
`$apply` is an **ordinary operator** with respect to the query format: its
|
|
399
|
+
result composes under the constructor rules like any other expression.
|
|
400
|
+
|
|
401
|
+
> **The `[]` idiom — the one trap every author hits.** An object member
|
|
402
|
+
> holds exactly one value, so the array-of-children idiom is
|
|
403
|
+
>
|
|
404
|
+
> ```json
|
|
405
|
+
> { "title": "$.title", "children": [ { "$apply": "$.chapters[*]" } ] }
|
|
406
|
+
> ```
|
|
407
|
+
>
|
|
408
|
+
> — **with** the array-constructor brackets, which splice the sequence into
|
|
409
|
+
> one array. The bare member form
|
|
410
|
+
> `"children": { "$apply": "$.chapters[*]" }` assigns the sequence itself
|
|
411
|
+
> to the member and is runtime error **JQ2001** the moment a node has two
|
|
412
|
+
> or more chapters (an object member takes exactly one value,
|
|
413
|
+
> QUERY-FORMAT §3.1). The prelude's sketch got this wrong; see §1.4.
|
|
414
|
+
|
|
415
|
+
### 6.4 Location propagation
|
|
416
|
+
|
|
417
|
+
Items selected by a selector that is a **path rooted at `$`** (the current
|
|
418
|
+
value) **or at `$root`** (§8.2) carry locations: the current value's (or
|
|
419
|
+
the root's) location extended with the path's normalized-path suffix. Such
|
|
420
|
+
items can match path rules (§3.2). Items produced any other way — FLWOR
|
|
421
|
+
phrases, operators, variable-rooted paths on other variables, literals —
|
|
422
|
+
are **location-less**: only schema and unconditional rules can match them.
|
|
423
|
+
The dispatch of the input root carries location `$`.
|
|
424
|
+
|
|
425
|
+
### 6.5 Mode dispatch defaults
|
|
426
|
+
|
|
427
|
+
An omitted mode means the **rule's own mode** — a static fact, since each
|
|
428
|
+
rule belongs to exactly one mode (§7). This deviates from XSLT 1.0, whose
|
|
429
|
+
modeless `apply-templates` always targets the unnamed mode; recursive walks
|
|
430
|
+
*within* a mode are the dominant pattern, and XSLT 3.0 grew
|
|
431
|
+
`mode="#current"` for exactly this reason. JSLT makes the common thing the
|
|
432
|
+
default.
|
|
433
|
+
|
|
434
|
+
Applying into a mode that has no rules is legal — the mode's disposition
|
|
435
|
+
does all the work (§5). A rule's `mode` is a single string in 0.1; mode
|
|
436
|
+
lists are roadmap.
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## 7. Modes
|
|
441
|
+
|
|
442
|
+
A rule's `mode` member (default: the unnamed mode `""`) partitions the rule
|
|
443
|
+
set. Each mode has its own ranked rule chain (§4), its own `unmatched`
|
|
444
|
+
disposition (§2.1, §5), and its own match pre-pass (§3.4). Modes are the
|
|
445
|
+
mechanism for walking the same input more than once with different
|
|
446
|
+
outputs — the same document rendered once as a table of contents and once
|
|
447
|
+
as body content is the canonical example (Appendix A.4).
|
|
448
|
+
|
|
449
|
+
Modes need not be declared: naming one in a rule's `mode` or an `$apply`
|
|
450
|
+
target creates it. The envelope's `modes` member (§2.1) exists only to
|
|
451
|
+
override a mode's disposition.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## 8. Externals and parameters
|
|
456
|
+
|
|
457
|
+
### 8.1 Stylesheet parameters
|
|
458
|
+
|
|
459
|
+
Free variables in rule bodies are the stylesheet's **parameters**, exactly
|
|
460
|
+
like query externals (QUERY-FORMAT §9): use is the declaration, and the
|
|
461
|
+
caller binds them at transformation time —
|
|
462
|
+
`transform(data, { rate: 1.21 })`. Evaluating a reference to an unbound
|
|
463
|
+
parameter is JQ2006 inside the body, surfacing wrapped as JT2004 (§10).
|
|
464
|
+
|
|
465
|
+
### 8.2 Reserved names: `root` and `path`
|
|
466
|
+
|
|
467
|
+
Two names are RESERVED and engine-bound per dispatch, shadowing any
|
|
468
|
+
caller-supplied binding of the same name:
|
|
469
|
+
|
|
470
|
+
- `root` — the input document root. `"$root"` and variable-rooted paths
|
|
471
|
+
like `"$root.currency"` read the whole input from any rule body.
|
|
472
|
+
- `path` — the current value's **normalized path** (RFC 9535 §2.7 string,
|
|
473
|
+
e.g. `"$['items'][0]['price']"`), or `null` for location-less values
|
|
474
|
+
(§6.4).
|
|
475
|
+
|
|
476
|
+
> **Contrast note.** The validator's `$query` keyword (the other direction
|
|
477
|
+
> of this stack: queries inside schemas) binds its `path` parameter as an
|
|
478
|
+
> **RFC 6901 JSON Pointer** — the schema-side convention. JSLT's matching
|
|
479
|
+
> language is JSONPath, so its `path` is an RFC 9535 **normalized path**.
|
|
480
|
+
> See the `@jarenjs/validate` README's "`$query` — cross-field assertions"
|
|
481
|
+
> section for the other half.
|
|
482
|
+
|
|
483
|
+
### 8.3 `transform.externals`
|
|
484
|
+
|
|
485
|
+
The compiled transformation exposes `transform.externals`: the **user
|
|
486
|
+
parameters only** (reserved names excluded), as the union across all rule
|
|
487
|
+
bodies, in order of first appearance.
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## 9. The type-test hook
|
|
492
|
+
|
|
493
|
+
```
|
|
494
|
+
compileJsltStylesheet(doc, { compileTypeTest: (schemaJson, docPath) => (value => boolean) })
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
One hook, three consumers: `schema` match conditions (§3.3), and the
|
|
498
|
+
`$valid`/`$assert` operators and `$as` clause inside rule bodies (the hook
|
|
499
|
+
is threaded through to every body compile). The contract, signature, and
|
|
500
|
+
once-per-literal-at-compile-time rule are exactly QUERY-FORMAT §8.11's;
|
|
501
|
+
`@jarenjs/validate/query`'s `createTypeTestCompiler` satisfies it, and the
|
|
502
|
+
`json` package never imports the validator — the dependency direction stays
|
|
503
|
+
validate → json.
|
|
504
|
+
|
|
505
|
+
A stylesheet using `schema` match conditions compiled **without** a hook is
|
|
506
|
+
compile error JT0006 (the mirror of JQ0008); a hook that rejects a match
|
|
507
|
+
schema (throws) is compile error JT0005 (the mirror of JQ0009). Inside
|
|
508
|
+
bodies, the query engine's own JQ0008/JQ0009 apply and surface as JT0007
|
|
509
|
+
(§10).
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## 10. Errors
|
|
514
|
+
|
|
515
|
+
### 10.1 Error objects
|
|
516
|
+
|
|
517
|
+
Consumers MUST raise compile-time errors as `JsltCompileError` and runtime
|
|
518
|
+
errors as `JsltRuntimeError`. Every error carries the same shape as the
|
|
519
|
+
query errors:
|
|
520
|
+
|
|
521
|
+
- `code` — a stable identifier from the registry below;
|
|
522
|
+
- `message` — human-readable, non-normative;
|
|
523
|
+
- `docPath` — an RFC 6901 JSON Pointer into the **stylesheet document**
|
|
524
|
+
(e.g. `/rules/2/match/path`);
|
|
525
|
+
- `cause` — the wrapped underlying error, where the registry says so.
|
|
526
|
+
|
|
527
|
+
In the bare-array shorthand (§2.1) the document has no `rules` member;
|
|
528
|
+
`docPath` pointers then start at the rule index (`/2/match/path`).
|
|
529
|
+
|
|
530
|
+
### 10.2 Registry
|
|
531
|
+
|
|
532
|
+
| code | condition |
|
|
533
|
+
|---|---|
|
|
534
|
+
| JT0001 | stylesheet shape: not array/object, missing `rules`, unknown envelope/mode member, bad `unmatched` value |
|
|
535
|
+
| JT0002 | rule shape: not an object, missing `body`, unknown member, `mode`/`priority` of the wrong type |
|
|
536
|
+
| JT0003 | match: invalid shape/empty object, path not a valid RFC 9535 query (cause = `JSONPathSyntaxError`) |
|
|
537
|
+
| JT0004 | unknown `$jslt` version |
|
|
538
|
+
| JT0005 | `schema` match rejected by the hook (cause preserved) |
|
|
539
|
+
| JT0006 | `schema` match without a `compileTypeTest` hook |
|
|
540
|
+
| JT0007 | body failed to compile — wraps `JsonQueryCompileError`; `docPath` = `/rules/<i>/body` + the inner docPath, `code`/cause preserved on `cause` |
|
|
541
|
+
| JT2001 | dispatch depth exceeded `maxDepth` |
|
|
542
|
+
| JT2002 | built-in rebuild: an object member's child dispatch produced 2+ items (message carries the location + member name) |
|
|
543
|
+
| JT2003 | unmatched value under `"error"` disposition |
|
|
544
|
+
| JT2004 | rule body raised a runtime error — wraps `JsonQueryRuntimeError` (cause), message names the rule (`/rules/<i>`) and the node location when known; never double-wraps |
|
|
545
|
+
|
|
546
|
+
JT0xxx are compile errors (`JsltCompileError`), JT2xxx runtime errors
|
|
547
|
+
(`JsltRuntimeError`) — the same numbering convention as JQ0xxx/JQ2xxx.
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## 11. Correspondence with XSLT (non-normative)
|
|
552
|
+
|
|
553
|
+
| XSLT | JSLT 0.1 |
|
|
554
|
+
|---|---|
|
|
555
|
+
| template rule (`xsl:template`) | rule object (§2.2) |
|
|
556
|
+
| `match` pattern (XPath) | `match.path` (RFC 9535 JSONPath, positional — §3.2) |
|
|
557
|
+
| schema-aware `type`/`element(*, T)` tests | `match.schema` (JSON Schema literal — §3.3) |
|
|
558
|
+
| `mode` | `mode` (§7) |
|
|
559
|
+
| `priority` + default-priority table | `priority` + the three-row default table (§4) |
|
|
560
|
+
| `xsl:apply-templates select="…" mode="…"` | `{"$apply": [selector, mode]}` (§6) |
|
|
561
|
+
| sequence constructor | rule body = Jaren JSON Query document |
|
|
562
|
+
| built-in rules / `on-no-match` | `unmatched` disposition per mode (§5) |
|
|
563
|
+
| `xsl:param` / `xsl:with-param` | stylesheet parameters = query externals (§8) |
|
|
564
|
+
|
|
565
|
+
Deliberate deviations, gathered:
|
|
566
|
+
|
|
567
|
+
1. **Per-value semantics, not on-no-match copy modes** (§5.2): rules apply
|
|
568
|
+
at every depth; `unmatched` only picks the between-matches behavior.
|
|
569
|
+
There is no deep-copy default.
|
|
570
|
+
2. **Static mode default** (§6.5): an omitted `$apply` mode targets the
|
|
571
|
+
rule's own mode (XSLT 3.0's `#current`), not the unnamed mode.
|
|
572
|
+
3. **The `[]` idiom** (§6.3): sequences compose under JSON constructor
|
|
573
|
+
rules; there is no implicit "children become content" as in XML tree
|
|
574
|
+
construction.
|
|
575
|
+
4. **No imports, no named templates, no stylesheet functions in 0.1** — all
|
|
576
|
+
roadmap. A stylesheet is one document; rule order and priority are the
|
|
577
|
+
only composition tools.
|
|
578
|
+
5. **A schema is a pattern** (the prelude's thesis): where XSLT 2.0 needed
|
|
579
|
+
schema-aware processors and typed-value ceremony to let patterns see
|
|
580
|
+
types, here the match condition and the type condition are the same
|
|
581
|
+
kind of object, compiled by the same hook.
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
## 12. API note (non-normative)
|
|
586
|
+
|
|
587
|
+
```
|
|
588
|
+
compileJsltStylesheet(doc, options) -> transform
|
|
589
|
+
transform(data, externals?) // plain JSON out
|
|
590
|
+
transform.externals // user parameter names (§8.3)
|
|
591
|
+
transform.doc // deeply frozen copy of the stylesheet
|
|
592
|
+
transformJson(stylesheet, data, externals?, options?) // one-shot, WeakMap-cached
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
Module: `@jarenjs/json/jslt`. `transform` returns plain JSON with the query
|
|
596
|
+
API's sequence mapping: `undefined` for the empty sequence, the item itself
|
|
597
|
+
for a singleton, an array of items for a longer sequence. `options` carries
|
|
598
|
+
`compileTypeTest` (§9) and `maxDepth` (§5.4), both for
|
|
599
|
+
`compileJsltStylesheet` and as the optional fourth argument of
|
|
600
|
+
`transformJson`. The one-shot function is the counterpart of `queryJson`,
|
|
601
|
+
caching compiled stylesheets by document identity and compile-option
|
|
602
|
+
values in a WeakMap.
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## Appendix A. Worked examples (normative fixtures)
|
|
607
|
+
|
|
608
|
+
Every example is complete and destined to run verbatim as engine tests and
|
|
609
|
+
schema fixtures. Unless noted, the disposition is the default `share` and
|
|
610
|
+
the mode is the unnamed mode.
|
|
611
|
+
|
|
612
|
+
### A.1 The empty stylesheet is the identity transform
|
|
613
|
+
|
|
614
|
+
```json
|
|
615
|
+
[]
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
Input:
|
|
619
|
+
|
|
620
|
+
```json
|
|
621
|
+
{ "store": { "book": [ { "title": "Sayings of the Century", "price": 8.95 } ] } }
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
Output: the input document — not a copy: `output === input` (§5.1).
|
|
625
|
+
|
|
626
|
+
### A.2 Surgical override — VAT on every price
|
|
627
|
+
|
|
628
|
+
```json
|
|
629
|
+
[ { "match": "$..price", "body": { "$mul": ["$", 1.21] } } ]
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
Input:
|
|
633
|
+
|
|
634
|
+
```json
|
|
635
|
+
{ "catalog": { "book": [ { "title": "A", "price": 10 }, { "title": "B", "price": 20 } ] },
|
|
636
|
+
"meta": { "publisher": { "name": "N" } } }
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
Output:
|
|
640
|
+
|
|
641
|
+
```json
|
|
642
|
+
{ "catalog": { "book": [ { "title": "A", "price": 12.1 }, { "title": "B", "price": 24.2 } ] },
|
|
643
|
+
"meta": { "publisher": { "name": "N" } } }
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
One rule, applied at every depth the path selects; every container on the
|
|
647
|
+
way is rebuilt by the built-in rule. Sharing is asserted: `output.meta ===
|
|
648
|
+
input.meta` — the untouched subtree is the input's own object (§5.1, §5.3).
|
|
649
|
+
|
|
650
|
+
### A.3 The book example, done right
|
|
651
|
+
|
|
652
|
+
```json
|
|
653
|
+
{ "$jslt": "0.1",
|
|
654
|
+
"rules": [
|
|
655
|
+
{ "match": { "schema": { "type": "object", "required": ["isbn"] } },
|
|
656
|
+
"body": { "title": "$.title",
|
|
657
|
+
"children": [ { "$apply": "$.chapters[*]" } ] } },
|
|
658
|
+
{ "match": { "schema": { "type": "object", "required": ["heading"] } },
|
|
659
|
+
"body": { "name": "$.heading" } }
|
|
660
|
+
] }
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
Input:
|
|
664
|
+
|
|
665
|
+
```json
|
|
666
|
+
{ "isbn": "0-553-21311-3", "title": "Moby Dick",
|
|
667
|
+
"chapters": [ { "heading": "Loomings" }, { "heading": "The Carpet-Bag" } ] }
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
Output:
|
|
671
|
+
|
|
672
|
+
```json
|
|
673
|
+
{ "title": "Moby Dick",
|
|
674
|
+
"children": [ { "name": "Loomings" }, { "name": "The Carpet-Bag" } ] }
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
The schema match fires on the root (shape, not position); `$apply`
|
|
678
|
+
dispatches each chapter, and the chapter rule fires on shape again. Note
|
|
679
|
+
the `[]` around the `$apply` — without it, this stylesheet is JQ2001 at
|
|
680
|
+
runtime, because two chapter results cannot occupy one member (§6.3).
|
|
681
|
+
|
|
682
|
+
### A.4 Two modes: table of contents + body rendering
|
|
683
|
+
|
|
684
|
+
```json
|
|
685
|
+
{ "$jslt": "0.1",
|
|
686
|
+
"rules": [
|
|
687
|
+
{ "match": "$",
|
|
688
|
+
"body": { "toc": [ { "$apply": ["$.sections[*]", "toc"] } ],
|
|
689
|
+
"body": [ { "$apply": ["$.sections[*]", "render"] } ] } },
|
|
690
|
+
{ "mode": "toc", "match": "$.sections[*]",
|
|
691
|
+
"body": { "ref": "$.id", "label": "$.heading" } },
|
|
692
|
+
{ "mode": "render", "match": "$.sections[*]",
|
|
693
|
+
"body": { "anchor": "$.id", "heading": "$.heading", "text": "$.text" } }
|
|
694
|
+
] }
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
Input:
|
|
698
|
+
|
|
699
|
+
```json
|
|
700
|
+
{ "title": "Guide",
|
|
701
|
+
"sections": [
|
|
702
|
+
{ "id": "intro", "heading": "Introduction", "text": "Start here." },
|
|
703
|
+
{ "id": "usage", "heading": "Usage", "text": "Then this." }
|
|
704
|
+
] }
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
Output:
|
|
708
|
+
|
|
709
|
+
```json
|
|
710
|
+
{ "toc": [ { "ref": "intro", "label": "Introduction" },
|
|
711
|
+
{ "ref": "usage", "label": "Usage" } ],
|
|
712
|
+
"body": [ { "anchor": "intro", "heading": "Introduction", "text": "Start here." },
|
|
713
|
+
{ "anchor": "usage", "heading": "Usage", "text": "Then this." } ] }
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
The same sections are walked twice — once per mode, each with its own rule
|
|
717
|
+
chain (§7). The selectors are `$`-rooted paths, so the section values carry
|
|
718
|
+
locations and match the path rules (§6.4).
|
|
719
|
+
|
|
720
|
+
### A.5 `fresh` — an annotated copy the caller may mutate
|
|
721
|
+
|
|
722
|
+
```json
|
|
723
|
+
{ "$jslt": "0.1",
|
|
724
|
+
"unmatched": "fresh",
|
|
725
|
+
"rules": [
|
|
726
|
+
{ "match": { "schema": { "type": "object", "required": ["price"] } },
|
|
727
|
+
"body": { "title": "$.title", "price": "$.price",
|
|
728
|
+
"taxed": { "$mul": ["$.price", 1.21] } } }
|
|
729
|
+
] }
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Input:
|
|
733
|
+
|
|
734
|
+
```json
|
|
735
|
+
{ "products": [ { "title": "A", "price": 10 } ] }
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
Output:
|
|
739
|
+
|
|
740
|
+
```json
|
|
741
|
+
{ "products": [ { "title": "A", "price": 10, "taxed": 12.1 } ] }
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
The schema rule rebuilds each priced object with an extra member; the
|
|
745
|
+
`fresh` disposition makes the built-in rule rebuild the surrounding
|
|
746
|
+
containers too (`output !== input`, `output.products !== input.products`),
|
|
747
|
+
so the caller owns the result tree — the forms/view-model case (§5.1).
|
|
748
|
+
|
|
749
|
+
### A.6 `error` — exhaustive dispatch with an explicit fallback
|
|
750
|
+
|
|
751
|
+
```json
|
|
752
|
+
{ "$jslt": "0.1",
|
|
753
|
+
"unmatched": "error",
|
|
754
|
+
"rules": [
|
|
755
|
+
{ "match": "$", "body": [ { "$apply": "$.events[*]" } ] },
|
|
756
|
+
{ "match": { "schema": { "type": "object", "required": ["error"] } },
|
|
757
|
+
"body": { "level": "fatal", "message": "$.error" } },
|
|
758
|
+
{ "match": { "schema": { "type": "object", "required": ["info"] } },
|
|
759
|
+
"body": { "level": "note", "message": "$.info" } },
|
|
760
|
+
{ "body": { "level": "unknown" } }
|
|
761
|
+
] }
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
Input:
|
|
765
|
+
|
|
766
|
+
```json
|
|
767
|
+
{ "events": [ { "info": "started" }, { "error": "disk full" }, { "beep": true } ] }
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
Output:
|
|
771
|
+
|
|
772
|
+
```json
|
|
773
|
+
[ { "level": "note", "message": "started" },
|
|
774
|
+
{ "level": "fatal", "message": "disk full" },
|
|
775
|
+
{ "level": "unknown" } ]
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
Under `"error"`, nothing passes silently: remove the unconditional fallback
|
|
779
|
+
rule (default priority `-1`, §4) and the same input raises JT2003 at
|
|
780
|
+
`$['events'][2]`. With it, unexpected shapes flow to an explicit default.
|
|
781
|
+
|
|
782
|
+
### A.7 Parameters and the reserved externals
|
|
783
|
+
|
|
784
|
+
```json
|
|
785
|
+
[ { "match": "$..price",
|
|
786
|
+
"body": { "amount": { "$mul": ["$", "$rate"] },
|
|
787
|
+
"currency": "$root.currency",
|
|
788
|
+
"at": "$path" } } ]
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
Called as `transform(input, { rate: 1.21 })`; `transform.externals` is
|
|
792
|
+
`["rate"]` — `root` and `path` are engine-bound and excluded (§8).
|
|
793
|
+
|
|
794
|
+
Input:
|
|
795
|
+
|
|
796
|
+
```json
|
|
797
|
+
{ "currency": "EUR", "items": [ { "sku": "a1", "price": 10 } ] }
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
Output:
|
|
801
|
+
|
|
802
|
+
```json
|
|
803
|
+
{ "currency": "EUR",
|
|
804
|
+
"items": [ { "sku": "a1",
|
|
805
|
+
"price": { "amount": 12.1, "currency": "EUR",
|
|
806
|
+
"at": "$['items'][0]['price']" } } ] }
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
`$rate` is a stylesheet parameter (§8.1); `$root.currency` reads the input
|
|
810
|
+
root from a deep rule; `$path` is the matched value's normalized path
|
|
811
|
+
(§8.2). Calling `transform(input)` without `rate` raises JT2004 wrapping
|
|
812
|
+
the body's JQ2006.
|
|
813
|
+
|
|
814
|
+
---
|
|
815
|
+
|
|
816
|
+
## Appendix B. LLM structured output (non-normative)
|
|
817
|
+
|
|
818
|
+
The complete stylesheet language is published as JSON Schema twins:
|
|
819
|
+
|
|
820
|
+
- [`../schemas/jaren-jslt.schema.json`](../schemas/jaren-jslt.schema.json) —
|
|
821
|
+
canonical draft 2020-12, `$id`
|
|
822
|
+
`https://jarenjs.dev/schemas/jaren-jslt/0.1`;
|
|
823
|
+
- [`../schemas/jaren-jslt.draft-07.schema.json`](../schemas/jaren-jslt.draft-07.schema.json) —
|
|
824
|
+
the mechanically derived draft-07 twin.
|
|
825
|
+
|
|
826
|
+
This extends the query format's
|
|
827
|
+
[structured-output story](./QUERY-FORMAT.md#appendix-b-llm-structured-output-non-normative)
|
|
828
|
+
to complete stylesheets. Constrained decoding against either artifact can
|
|
829
|
+
prevent unknown envelope/rule members, missing bodies, malformed match
|
|
830
|
+
objects, bad dispositions and versions, unknown body operators, and wrong
|
|
831
|
+
structurally expressible `$apply` arities before any compiler runs.
|
|
832
|
+
|
|
833
|
+
The rule-body grammar is not a hand-maintained copy. The canonical
|
|
834
|
+
stylesheet artifact deep-copies the committed query artifact's definition
|
|
835
|
+
map, adds the single body-local `applyPhrase`, and appends that phrase to
|
|
836
|
+
`objectExpression.oneOf`. Tests pin that derivation and separately pin the
|
|
837
|
+
canonical-to-draft-07 transform. A query-schema refactor therefore fails
|
|
838
|
+
the artifact test loudly instead of letting the stylesheet grammar drift;
|
|
839
|
+
the published query artifacts themselves remain unchanged.
|
|
840
|
+
|
|
841
|
+
As with generated queries, schema-valid does not mean semantically complete.
|
|
842
|
+
The compiler remains authoritative for rule ranking, reserved externals,
|
|
843
|
+
type-test hook availability and schema compilation, plus the positional
|
|
844
|
+
fact that the second item of `{"$apply": [selector, mode]}` is a literal
|
|
845
|
+
string. That last fact cannot be represented without tuple validation,
|
|
846
|
+
which the draft-neutral artifact policy deliberately excludes. The
|
|
847
|
+
remaining failures are the JT0xxx/JT2xxx errors of §10, carrying a
|
|
848
|
+
`docPath` into the stylesheet for a repair loop.
|
|
849
|
+
|
|
850
|
+
Provider "structured output" implementations also support different JSON
|
|
851
|
+
Schema subsets regardless of the draft they advertise. In particular,
|
|
852
|
+
recursive references, `patternProperties`, `propertyNames`, `format`, and
|
|
853
|
+
some composition keywords may be restricted or treated as annotations.
|
|
854
|
+
Always validate a generated stylesheet locally against the full artifact
|
|
855
|
+
before calling `compileJsltStylesheet`. A simplified lowest-common-
|
|
856
|
+
denominator LLM profile could trade precision for broader provider support,
|
|
857
|
+
but no such third artifact is defined in version 0.1.
|
|
858
|
+
|
|
859
|
+
Stylesheets remain ordinary JSON throughout the toolchain: they can be
|
|
860
|
+
function-call arguments, retrieved rule sets, reviewed diffs, audit-log
|
|
861
|
+
entries, and replayable transformation programs without a text parser.
|