@jarenjs/json 0.9.2 → 0.34.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 +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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The statistics pack — aggregators core does not ship as
|
|
4
|
+
* operators. `sum`/`min`/`max`/`avg`/`count` are ALREADY core query
|
|
5
|
+
* operators and are NOT re-registered here; this pack adds the summaries
|
|
6
|
+
* they don't cover. The helpers are small pure functions defined in this
|
|
7
|
+
* file (their natural home — core carries no statistics module), each a
|
|
8
|
+
* deterministic function of its array argument.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** @param {number[]} xs */
|
|
12
|
+
function mean(xs) {
|
|
13
|
+
if (xs.length === 0) return undefined;
|
|
14
|
+
let s = 0;
|
|
15
|
+
for (const x of xs) s += x;
|
|
16
|
+
return s / xs.length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Sample variance (n−1). @param {number[]} xs */
|
|
20
|
+
function variance(xs) {
|
|
21
|
+
if (xs.length < 2) return undefined;
|
|
22
|
+
const m = mean(xs);
|
|
23
|
+
let s = 0;
|
|
24
|
+
for (const x of xs) s += (x - m) * (x - m);
|
|
25
|
+
return s / (xs.length - 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @param {number[]} xs */
|
|
29
|
+
function stddev(xs) {
|
|
30
|
+
const v = variance(xs);
|
|
31
|
+
return v === undefined ? undefined : Math.sqrt(v);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @param {number[]} xs */
|
|
35
|
+
function median(xs) {
|
|
36
|
+
if (xs.length === 0) return undefined;
|
|
37
|
+
const sorted = xs.slice().sort((a, b) => a - b);
|
|
38
|
+
const mid = sorted.length >> 1;
|
|
39
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Linear-interpolation percentile; `p` in [0, 100]. @param {number[]} xs */
|
|
43
|
+
function percentile(xs, p) {
|
|
44
|
+
if (xs.length === 0) return undefined;
|
|
45
|
+
const sorted = xs.slice().sort((a, b) => a - b);
|
|
46
|
+
if (sorted.length === 1) return sorted[0];
|
|
47
|
+
const rank = (Math.max(0, Math.min(100, p)) / 100) * (sorted.length - 1);
|
|
48
|
+
const lo = Math.floor(rank);
|
|
49
|
+
const hi = Math.ceil(rank);
|
|
50
|
+
if (lo === hi) return sorted[lo];
|
|
51
|
+
return sorted[lo] + (sorted[hi] - sorted[lo]) * (rank - lo);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const aggNum = (signature, fn) => ({ kind: 'agg', signature, result: 'number', fn, pushable: false });
|
|
55
|
+
|
|
56
|
+
export const statsPack = {
|
|
57
|
+
name: 'stats',
|
|
58
|
+
entries: {
|
|
59
|
+
$mean: aggNum(['seq<number>'], mean),
|
|
60
|
+
$median: aggNum(['seq<number>'], median),
|
|
61
|
+
$variance: aggNum(['seq<number>'], variance),
|
|
62
|
+
$stddev: aggNum(['seq<number>'], stddev),
|
|
63
|
+
$percentile: aggNum(['seq<number>', 'number'], percentile),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The JSLT operator/aggregator registry (Ring 1): the
|
|
4
|
+
* `@jarenjs/formats` + `JarenValidator.addFormats` experience for the
|
|
5
|
+
* query/JSLT vocabulary. A caller composes packs of pure functions
|
|
6
|
+
* (`@jarenjs/core` math, finance, statistics) into a registry and gets
|
|
7
|
+
* a compiler bound to them — the operators then work in JSLT, in the
|
|
8
|
+
* query engine, and in linq-over-memory (which compiles to query
|
|
9
|
+
* documents).
|
|
10
|
+
*
|
|
11
|
+
* The mechanism is the engine's existing `options.extensions` seam (the
|
|
12
|
+
* same one the JSLT layer uses internally for `$apply`) plus
|
|
13
|
+
* `options.functions` (the `$call` registry). This file only TRANSLATES
|
|
14
|
+
* plain-data pack entries into those two shapes and holds them in an
|
|
15
|
+
* immutable-by-copy builder — so a pack never imports the operator ABI,
|
|
16
|
+
* exactly the decoupling `@jarenjs/formats` has from `@jarenjs/validate`.
|
|
17
|
+
*
|
|
18
|
+
* Entry kinds: `op` (scalar `$`-operator over scalar
|
|
19
|
+
* operands), `agg` (an operator whose declared `seq` operands are folded
|
|
20
|
+
* to arrays before the call — the generalization of core `$sum`'s fold),
|
|
21
|
+
* and `fn` (a bare `$call` function, the low-level escape). The published
|
|
22
|
+
* closed vocabulary is unchanged: without a registry a document using a
|
|
23
|
+
* pack operator fails `JQ0002` exactly as before.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { EMPTY, Seq, seqOf, firstItem } from '../query/runtime.js';
|
|
27
|
+
import { CARD_OPT, CARD_MANY, isReservedQueryName } from '../query/normalize.js';
|
|
28
|
+
import { JsonQueryRuntimeError } from '../query/errors.js';
|
|
29
|
+
import { compileJsonQuery } from '../query/index.js';
|
|
30
|
+
import { compileJsltStylesheet } from './index.js';
|
|
31
|
+
|
|
32
|
+
const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
33
|
+
const RESULT_OPT = () => CARD_OPT;
|
|
34
|
+
const RESULT_MANY = () => CARD_MANY;
|
|
35
|
+
const RT_NUMBER = () => 'number';
|
|
36
|
+
|
|
37
|
+
/** Normalize an operand-kind token to `'seq'` or `'scalar'`. */
|
|
38
|
+
function operandKind(token) {
|
|
39
|
+
return typeof token === 'string' && token.startsWith('seq') ? 'seq' : 'scalar';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Translate one pack entry into an `options.extensions` operator entry
|
|
44
|
+
* (the shape `query/operators.js` uses). The compile gathers each `seq`
|
|
45
|
+
* operand into a JS array (`EMPTY`→`[]`, a lone value→`[value]`, a
|
|
46
|
+
* `Seq`→its items) and reads each scalar operand as a single value
|
|
47
|
+
* (`EMPTY` propagates to an empty result), calls the pure function, and
|
|
48
|
+
* wraps the result — a scalar as one item, an array under a `seq` result
|
|
49
|
+
* as a `Seq`. A throwing function becomes the coded runtime error
|
|
50
|
+
* `JQ2010`, never a crash.
|
|
51
|
+
* @param {string} name
|
|
52
|
+
* @param {{ signature: string[], result: string, fn: Function }} entry
|
|
53
|
+
*/
|
|
54
|
+
function toExtensionEntry(name, entry) {
|
|
55
|
+
const sig = entry.signature.map(operandKind);
|
|
56
|
+
const arity = sig.length;
|
|
57
|
+
const seqResult = operandKind(entry.result) === 'seq';
|
|
58
|
+
const fn = entry.fn;
|
|
59
|
+
const params = arity === 1
|
|
60
|
+
? 'expr'
|
|
61
|
+
: { kinds: new Array(arity).fill('expr'), min: arity };
|
|
62
|
+
return {
|
|
63
|
+
params,
|
|
64
|
+
result: seqResult ? RESULT_MANY : RESULT_OPT,
|
|
65
|
+
resultType: RT_NUMBER,
|
|
66
|
+
compile: (gets, args, docPath) => (f) => {
|
|
67
|
+
const call = new Array(arity);
|
|
68
|
+
for (let i = 0; i < arity; i++) {
|
|
69
|
+
const v = gets[i](f);
|
|
70
|
+
if (sig[i] === 'seq') {
|
|
71
|
+
call[i] = v === EMPTY ? [] : v instanceof Seq ? v.items.slice() : [v];
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
if (v === EMPTY) return EMPTY; // a missing scalar operand → empty result
|
|
75
|
+
call[i] = v instanceof Seq ? firstItem(v) : v;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
let out;
|
|
79
|
+
try {
|
|
80
|
+
out = fn(...call);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
throw new JsonQueryRuntimeError('JQ2010',
|
|
84
|
+
`registered operator '${name}' failed: ${err?.message ?? String(err)}`,
|
|
85
|
+
docPath);
|
|
86
|
+
}
|
|
87
|
+
if (seqResult) {
|
|
88
|
+
if (!Array.isArray(out)) return out === undefined || out === null ? EMPTY : out;
|
|
89
|
+
return out.length === 0 ? EMPTY : seqOf(out);
|
|
90
|
+
}
|
|
91
|
+
return out === undefined ? EMPTY : out;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Validate a pack's shape (host programming error → TypeError). */
|
|
97
|
+
function validatePack(pack) {
|
|
98
|
+
if (pack === null || typeof pack !== 'object'
|
|
99
|
+
|| typeof pack.name !== 'string' || pack.name === ''
|
|
100
|
+
|| pack.entries === null || typeof pack.entries !== 'object') {
|
|
101
|
+
throw new TypeError('a pack must be { name: string, entries: object }');
|
|
102
|
+
}
|
|
103
|
+
for (const [name, entry] of Object.entries(pack.entries)) {
|
|
104
|
+
if (entry === null || typeof entry !== 'object' || typeof entry.fn !== 'function') {
|
|
105
|
+
throw new TypeError(`pack '${pack.name}' entry '${name}' must carry a function 'fn'`);
|
|
106
|
+
}
|
|
107
|
+
const kind = entry.kind ?? (entry.signature?.some((s) => operandKind(s) === 'seq') ? 'agg' : 'op');
|
|
108
|
+
if (kind !== 'fn') {
|
|
109
|
+
if (name.charCodeAt(0) !== 0x24) {
|
|
110
|
+
throw new TypeError(`pack '${pack.name}' operator '${name}' must start with '$'`);
|
|
111
|
+
}
|
|
112
|
+
if (!Array.isArray(entry.signature) || entry.signature.length === 0) {
|
|
113
|
+
throw new TypeError(`pack '${pack.name}' operator '${name}' needs a non-empty signature`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build an immutable JSLT operator registry. `.use(pack)` returns a NEW
|
|
121
|
+
* registry with the pack merged (a value, not a mutable singleton, so
|
|
122
|
+
* the functional spirit of the compilers is preserved). A name that
|
|
123
|
+
* collides with the core vocabulary, or with an already-registered name,
|
|
124
|
+
* throws a `TypeError` at `.use()` time — a host programming error, never
|
|
125
|
+
* a `JQ` document error.
|
|
126
|
+
* @param {{ extensions: Record<string, any>, functions: Record<string, Function>,
|
|
127
|
+
* meta: Record<string, any>, packs: string[] }} [state]
|
|
128
|
+
*/
|
|
129
|
+
export function createJsltRegistry(state) {
|
|
130
|
+
const base = state ?? { extensions: {}, functions: {}, meta: {}, packs: [] };
|
|
131
|
+
|
|
132
|
+
const use = (pack) => {
|
|
133
|
+
validatePack(pack);
|
|
134
|
+
const extensions = { ...base.extensions };
|
|
135
|
+
const functions = { ...base.functions };
|
|
136
|
+
const meta = { ...base.meta };
|
|
137
|
+
for (const [name, entry] of Object.entries(pack.entries)) {
|
|
138
|
+
if (hasOwn(extensions, name) || hasOwn(functions, name)) {
|
|
139
|
+
throw new TypeError(`operator '${name}' is already registered (pack '${pack.name}')`);
|
|
140
|
+
}
|
|
141
|
+
const kind = entry.kind
|
|
142
|
+
?? (entry.signature?.some((s) => operandKind(s) === 'seq') ? 'agg' : 'op');
|
|
143
|
+
if (kind === 'fn') {
|
|
144
|
+
functions[name] = entry.fn;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
if (isReservedQueryName(name)) {
|
|
148
|
+
throw new TypeError(`operator '${name}' collides with the core vocabulary`);
|
|
149
|
+
}
|
|
150
|
+
extensions[name] = toExtensionEntry(name, entry);
|
|
151
|
+
}
|
|
152
|
+
meta[name] = {
|
|
153
|
+
pack: pack.name, kind,
|
|
154
|
+
signature: entry.signature ?? null, result: entry.result ?? null,
|
|
155
|
+
pushable: entry.pushable ?? false, fn: entry.fn,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return createJsltRegistry({
|
|
159
|
+
extensions, functions, meta, packs: [...base.packs, pack.name],
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// frozen once per registry instance, so identity-based compile caching
|
|
164
|
+
// (jslt/index.js cachedTransform) hits across calls with the same
|
|
165
|
+
// registry; only a caller's OWN extensions/functions force a fresh
|
|
166
|
+
// merged object.
|
|
167
|
+
const frozenExtensions = Object.freeze({ ...base.extensions });
|
|
168
|
+
const frozenFunctions = Object.freeze({ ...base.functions });
|
|
169
|
+
|
|
170
|
+
/** The `{ extensions, functions }` a compile call needs, merged over a
|
|
171
|
+
* caller's own options — the stable frozen objects when the caller
|
|
172
|
+
* added none (the common case). */
|
|
173
|
+
const mergedOptions = (opts) => {
|
|
174
|
+
const userExt = opts?.extensions && Object.keys(opts.extensions).length > 0;
|
|
175
|
+
const userFns = opts?.functions && Object.keys(opts.functions).length > 0;
|
|
176
|
+
return {
|
|
177
|
+
...opts,
|
|
178
|
+
extensions: userExt ? { ...opts.extensions, ...base.extensions } : frozenExtensions,
|
|
179
|
+
functions: userFns ? { ...opts.functions, ...base.functions } : frozenFunctions,
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return Object.freeze({
|
|
184
|
+
use,
|
|
185
|
+
/** Every registered operator/function name (for docs, AI, errors). */
|
|
186
|
+
names: () => [...Object.keys(base.extensions), ...Object.keys(base.functions)],
|
|
187
|
+
/** The raw `{ extensions, functions }` for a manual compile call. */
|
|
188
|
+
toOptions: () => ({ extensions: { ...base.extensions }, functions: { ...base.functions } }),
|
|
189
|
+
/** The SQL-pushable subset, as `{ name -> meta }` (consumed by the db
|
|
190
|
+
* in Rings 2–3; here it is just the data). */
|
|
191
|
+
forSql: () => Object.fromEntries(
|
|
192
|
+
Object.entries(base.meta).filter(([, m]) => m.pushable !== false)),
|
|
193
|
+
/** The full registration metadata (Rings 2–3, tooling). */
|
|
194
|
+
describe: () => ({ ...base.meta }),
|
|
195
|
+
/** Compile a JSLT stylesheet bound to this registry. */
|
|
196
|
+
compile: (stylesheet, opts) => compileJsltStylesheet(stylesheet, mergedOptions(opts)),
|
|
197
|
+
/** Compile a bare query document bound to this registry (linq-over-memory). */
|
|
198
|
+
compileQuery: (document, opts) => compileJsonQuery(document, mergedOptions(opts)),
|
|
199
|
+
});
|
|
200
|
+
}
|
package/src/jslt/stylesheet.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// schema, and body closures are compiled by dispatch.js.
|
|
5
5
|
|
|
6
6
|
import { JsltCompileError } from './errors.js';
|
|
7
|
+
import { failerFor } from '../errors.js';
|
|
8
|
+
import { encodeJSONPointerSegment } from '../pointer.js';
|
|
9
|
+
import { isJsonObject } from '@jarenjs/core/object';
|
|
7
10
|
|
|
8
11
|
const hasOwn = Object.hasOwn;
|
|
9
12
|
const ENVELOPE_KEYS = new Set(['$jslt', 'rules', 'unmatched', 'modes']);
|
|
@@ -11,40 +14,28 @@ const RULE_KEYS = new Set(['match', 'mode', 'priority', 'body']);
|
|
|
11
14
|
const MATCH_KEYS = new Set(['path', 'schema']);
|
|
12
15
|
const MODE_KEYS = new Set(['unmatched']);
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function escapeToken(token) {
|
|
19
|
-
if (token.indexOf('~') < 0 && token.indexOf('/') < 0)
|
|
20
|
-
return token;
|
|
21
|
-
return token.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function fail(code, message, docPath) {
|
|
25
|
-
throw new JsltCompileError(code, message, docPath);
|
|
26
|
-
}
|
|
17
|
+
const fail = failerFor(JsltCompileError);
|
|
27
18
|
|
|
28
19
|
function isDisposition(value) {
|
|
29
20
|
return value === 'share' || value === 'fresh' || value === 'error';
|
|
30
21
|
}
|
|
31
22
|
|
|
32
23
|
function normalizeModes(value, docPath) {
|
|
33
|
-
if (!
|
|
24
|
+
if (!isJsonObject(value))
|
|
34
25
|
fail('JT0001', "'modes' must be an object", docPath);
|
|
35
26
|
const modes = new Map();
|
|
36
27
|
const names = Object.keys(value);
|
|
37
28
|
for (let i = 0; i < names.length; i++) {
|
|
38
29
|
const name = names[i];
|
|
39
|
-
const modePath = docPath + '/' +
|
|
30
|
+
const modePath = docPath + '/' + encodeJSONPointerSegment(name);
|
|
40
31
|
const config = value[name];
|
|
41
|
-
if (!
|
|
32
|
+
if (!isJsonObject(config))
|
|
42
33
|
fail('JT0001', `mode '${name}' must be an object`, modePath);
|
|
43
34
|
const keys = Object.keys(config);
|
|
44
35
|
for (let j = 0; j < keys.length; j++) {
|
|
45
36
|
if (!MODE_KEYS.has(keys[j]))
|
|
46
37
|
fail('JT0001', `unknown mode member '${keys[j]}'`,
|
|
47
|
-
modePath + '/' +
|
|
38
|
+
modePath + '/' + encodeJSONPointerSegment(keys[j]));
|
|
48
39
|
}
|
|
49
40
|
if (!hasOwn(config, 'unmatched'))
|
|
50
41
|
fail('JT0001', `mode '${name}' requires 'unmatched'`, modePath + '/unmatched');
|
|
@@ -69,7 +60,7 @@ function normalizeMatch(value, matchPath) {
|
|
|
69
60
|
schemaDocPath: '',
|
|
70
61
|
});
|
|
71
62
|
}
|
|
72
|
-
if (!
|
|
63
|
+
if (!isJsonObject(value))
|
|
73
64
|
fail('JT0003', "'match' must be a JSONPath string or an object", matchPath);
|
|
74
65
|
|
|
75
66
|
const keys = Object.keys(value);
|
|
@@ -78,7 +69,7 @@ function normalizeMatch(value, matchPath) {
|
|
|
78
69
|
for (let i = 0; i < keys.length; i++) {
|
|
79
70
|
if (!MATCH_KEYS.has(keys[i]))
|
|
80
71
|
fail('JT0003', `unknown match member '${keys[i]}'`,
|
|
81
|
-
matchPath + '/' +
|
|
72
|
+
matchPath + '/' + encodeJSONPointerSegment(keys[i]));
|
|
82
73
|
}
|
|
83
74
|
const hasPath = hasOwn(value, 'path');
|
|
84
75
|
const hasSchema = hasOwn(value, 'schema');
|
|
@@ -98,14 +89,14 @@ function normalizeMatch(value, matchPath) {
|
|
|
98
89
|
|
|
99
90
|
function normalizeRule(value, index, rulesPath) {
|
|
100
91
|
const rulePath = rulesPath + '/' + index;
|
|
101
|
-
if (!
|
|
92
|
+
if (!isJsonObject(value))
|
|
102
93
|
fail('JT0002', 'a stylesheet rule must be an object', rulePath);
|
|
103
94
|
|
|
104
95
|
const keys = Object.keys(value);
|
|
105
96
|
for (let i = 0; i < keys.length; i++) {
|
|
106
97
|
if (!RULE_KEYS.has(keys[i]))
|
|
107
98
|
fail('JT0002', `unknown rule member '${keys[i]}'`,
|
|
108
|
-
rulePath + '/' +
|
|
99
|
+
rulePath + '/' + encodeJSONPointerSegment(keys[i]));
|
|
109
100
|
}
|
|
110
101
|
if (!hasOwn(value, 'body'))
|
|
111
102
|
fail('JT0002', "a stylesheet rule requires 'body'", rulePath + '/body');
|
|
@@ -153,12 +144,12 @@ export function normalizeJsltStylesheet(doc) {
|
|
|
153
144
|
sourceRules = doc;
|
|
154
145
|
rulesPath = '';
|
|
155
146
|
}
|
|
156
|
-
else if (
|
|
147
|
+
else if (isJsonObject(doc)) {
|
|
157
148
|
const keys = Object.keys(doc);
|
|
158
149
|
for (let i = 0; i < keys.length; i++) {
|
|
159
150
|
if (!ENVELOPE_KEYS.has(keys[i]))
|
|
160
151
|
fail('JT0001', `unknown stylesheet member '${keys[i]}'`,
|
|
161
|
-
'/' +
|
|
152
|
+
'/' + encodeJSONPointerSegment(keys[i]));
|
|
162
153
|
}
|
|
163
154
|
if (!hasOwn(doc, '$jslt'))
|
|
164
155
|
fail('JT0001', "the stylesheet envelope requires '$jslt'", '/$jslt');
|
package/src/jtlt/desugar.js
CHANGED
|
@@ -20,13 +20,12 @@
|
|
|
20
20
|
// pair unambiguously.
|
|
21
21
|
|
|
22
22
|
import { JtltCompileError } from './errors.js';
|
|
23
|
+
import { failerFor } from '../errors.js';
|
|
23
24
|
|
|
24
25
|
const DOLLAR = 0x24;
|
|
25
26
|
const BUILTIN_PRIORITY = -1e308;
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
throw new JtltCompileError(code, message, docPath);
|
|
29
|
-
}
|
|
28
|
+
const fail = failerFor(JtltCompileError);
|
|
30
29
|
|
|
31
30
|
// Collect every statically declared `$apply` target mode inside a plain
|
|
32
31
|
// JSON expression tree. `$apply` mode arguments are literal strings by
|
package/src/jtlt/errors.js
CHANGED
|
@@ -3,17 +3,15 @@
|
|
|
3
3
|
// stable `code` and a `docPath`, an RFC 6901 JSON Pointer into the template
|
|
4
4
|
// document. Wrapped JSLT/query errors are exposed through `cause`.
|
|
5
5
|
|
|
6
|
+
import { CodedDocPathError } from '../errors.js';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Error thrown when a JTLT template is rejected at compile time
|
|
8
10
|
* (`TL0xxx` codes).
|
|
9
11
|
*/
|
|
10
|
-
export class JtltCompileError extends
|
|
12
|
+
export class JtltCompileError extends CodedDocPathError {
|
|
11
13
|
constructor(code, message, docPath, cause = undefined) {
|
|
12
|
-
super(
|
|
13
|
-
cause === undefined ? undefined : { cause });
|
|
14
|
-
this.name = 'JtltCompileError';
|
|
15
|
-
this.code = code;
|
|
16
|
-
this.docPath = docPath;
|
|
14
|
+
super('JtltCompileError', code, message, docPath, cause);
|
|
17
15
|
}
|
|
18
16
|
}
|
|
19
17
|
|
|
@@ -21,13 +19,9 @@ export class JtltCompileError extends Error {
|
|
|
21
19
|
* Error thrown when rendering with a compiled JTLT template fails
|
|
22
20
|
* (`TL2xxx` codes).
|
|
23
21
|
*/
|
|
24
|
-
export class JtltRuntimeError extends
|
|
22
|
+
export class JtltRuntimeError extends CodedDocPathError {
|
|
25
23
|
constructor(code, message, docPath, cause = undefined) {
|
|
26
|
-
super(
|
|
27
|
-
cause === undefined ? undefined : { cause });
|
|
28
|
-
this.name = 'JtltRuntimeError';
|
|
29
|
-
this.code = code;
|
|
30
|
-
this.docPath = docPath;
|
|
24
|
+
super('JtltRuntimeError', code, message, docPath, cause);
|
|
31
25
|
}
|
|
32
26
|
}
|
|
33
27
|
|
package/src/jtlt/index.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// reimplemented.
|
|
9
9
|
|
|
10
10
|
import { deepFreezeCopy } from '../query/normalize.js';
|
|
11
|
+
import { createOptionVariantCache, identityOf } from '../option-variants.js';
|
|
11
12
|
import {
|
|
12
13
|
compileJsltStylesheet,
|
|
13
14
|
JsltCompileError,
|
|
@@ -85,41 +86,23 @@ export function compileJtltStylesheet(doc, options = {}) {
|
|
|
85
86
|
return render;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
const TEMPLATE_CACHE =
|
|
89
|
+
const TEMPLATE_CACHE = createOptionVariantCache();
|
|
89
90
|
|
|
90
91
|
function cachedRender(template, options) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
};
|
|
97
|
-
TEMPLATE_CACHE.set(template, record);
|
|
98
|
-
}
|
|
99
|
-
|
|
92
|
+
// The FULL option tuple keys the cache. `compileJtltStylesheet`
|
|
93
|
+
// forwards its options to the JSLT compiler, which also honours
|
|
94
|
+
// `memo` and `pathFunctions` — the old two-field comparison dropped
|
|
95
|
+
// both, so a one-call render with `pathFunctions` compiled without
|
|
96
|
+
// them (a live cache-poisoning bug, fixed by keying on everything).
|
|
100
97
|
const compileTypeTest = typeof options?.compileTypeTest === 'function'
|
|
101
98
|
? options.compileTypeTest
|
|
102
99
|
: null;
|
|
103
100
|
const maxDepth = options?.maxDepth === undefined ? 1024 : options.maxDepth;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
let variants = record.variants;
|
|
111
|
-
if (variants === null) {
|
|
112
|
-
variants = [];
|
|
113
|
-
record.variants = variants;
|
|
114
|
-
}
|
|
115
|
-
for (let i = 0; i < variants.length; i++) {
|
|
116
|
-
const variant = variants[i];
|
|
117
|
-
if (variant.compileTypeTest === compileTypeTest && variant.maxDepth === maxDepth)
|
|
118
|
-
return variant.render;
|
|
119
|
-
}
|
|
120
|
-
const render = compileJtltStylesheet(template, options);
|
|
121
|
-
variants.push({ compileTypeTest, maxDepth, render });
|
|
122
|
-
return render;
|
|
101
|
+
const memo = options?.memo === true;
|
|
102
|
+
const pathFunctions = options?.pathFunctions == null ? null : options.pathFunctions;
|
|
103
|
+
const key = `${identityOf(compileTypeTest)}|${maxDepth}|${memo ? 1 : 0}|${identityOf(pathFunctions)}`;
|
|
104
|
+
return TEMPLATE_CACHE.getOrCompile(template, key,
|
|
105
|
+
() => compileJtltStylesheet(template, options));
|
|
123
106
|
}
|
|
124
107
|
|
|
125
108
|
/**
|
package/src/jtlt/template.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
// drift apart.
|
|
7
7
|
|
|
8
8
|
import { JtltCompileError } from './errors.js';
|
|
9
|
+
import { failerFor } from '../errors.js';
|
|
10
|
+
import { encodeJSONPointerSegment } from '../pointer.js';
|
|
11
|
+
import { isJsonObject } from '@jarenjs/core/object';
|
|
9
12
|
|
|
10
13
|
const hasOwn = Object.hasOwn;
|
|
11
14
|
const ENVELOPE_KEYS = new Set(['$jtlt', 'output', 'rules']);
|
|
@@ -16,30 +19,18 @@ const OUTPUT_METHODS = new Set(['text', 'xml']);
|
|
|
16
19
|
// built-in rules, which sit at -1e308 - beneath every user rule.
|
|
17
20
|
export const RESERVED_PRIORITY_FLOOR = -1e307;
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function escapeToken(token) {
|
|
24
|
-
if (token.indexOf('~') < 0 && token.indexOf('/') < 0)
|
|
25
|
-
return token;
|
|
26
|
-
return token.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function fail(code, message, docPath) {
|
|
30
|
-
throw new JtltCompileError(code, message, docPath);
|
|
31
|
-
}
|
|
22
|
+
const fail = failerFor(JtltCompileError);
|
|
32
23
|
|
|
33
24
|
function normalizeRule(value, index, rulesPath) {
|
|
34
25
|
const rulePath = rulesPath + '/' + index;
|
|
35
|
-
if (!
|
|
26
|
+
if (!isJsonObject(value))
|
|
36
27
|
fail('TL0002', 'a template rule must be an object', rulePath);
|
|
37
28
|
|
|
38
29
|
const keys = Object.keys(value);
|
|
39
30
|
for (let i = 0; i < keys.length; i++) {
|
|
40
31
|
if (!RULE_KEYS.has(keys[i]))
|
|
41
32
|
fail('TL0002', `unknown rule member '${keys[i]}'`,
|
|
42
|
-
rulePath + '/' +
|
|
33
|
+
rulePath + '/' + encodeJSONPointerSegment(keys[i]));
|
|
43
34
|
}
|
|
44
35
|
if (!hasOwn(value, 'body'))
|
|
45
36
|
fail('TL0002', "a template rule requires 'body'", rulePath + '/body');
|
|
@@ -56,7 +47,7 @@ function normalizeRule(value, index, rulesPath) {
|
|
|
56
47
|
rulePath + '/priority');
|
|
57
48
|
}
|
|
58
49
|
const hasMatch = hasOwn(value, 'match');
|
|
59
|
-
if (hasMatch && typeof value.match !== 'string' && !
|
|
50
|
+
if (hasMatch && typeof value.match !== 'string' && !isJsonObject(value.match))
|
|
60
51
|
fail('TL0002', "'match' must be a JSONPath string or an object", rulePath + '/match');
|
|
61
52
|
|
|
62
53
|
return Object.freeze({
|
|
@@ -87,12 +78,12 @@ export function normalizeJtltTemplate(doc) {
|
|
|
87
78
|
sourceRules = doc;
|
|
88
79
|
rulesPath = '';
|
|
89
80
|
}
|
|
90
|
-
else if (
|
|
81
|
+
else if (isJsonObject(doc)) {
|
|
91
82
|
const keys = Object.keys(doc);
|
|
92
83
|
for (let i = 0; i < keys.length; i++) {
|
|
93
84
|
if (!ENVELOPE_KEYS.has(keys[i]))
|
|
94
85
|
fail('TL0001', `unknown template member '${keys[i]}'`,
|
|
95
|
-
'/' +
|
|
86
|
+
'/' + encodeJSONPointerSegment(keys[i]));
|
|
96
87
|
}
|
|
97
88
|
if (!hasOwn(doc, '$jtlt'))
|
|
98
89
|
fail('TL0001', "the template envelope requires '$jtlt'", '/$jtlt');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Per-document compilation-variant caching, shared by the JSLT
|
|
4
|
+
* and JTLT one-call entry points. A document compiles differently under
|
|
5
|
+
* different options, so the cache key is the FULL option tuple — every
|
|
6
|
+
* option that changes what compiles must be part of the derived key, or
|
|
7
|
+
* a second call with different options silently reuses the first
|
|
8
|
+
* compilation (the cache-poisoning bug the JTLT cache had before the
|
|
9
|
+
* health pass). Identity-compared option values (hook functions,
|
|
10
|
+
* function-extension registries) are interned to stable per-process ids
|
|
11
|
+
* so the key is a flat string and lookup is O(1) instead of a linear
|
|
12
|
+
* scan over an unbounded variants array.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createBoundedCache, createWeakCache } from '@jarenjs/core/cache';
|
|
16
|
+
|
|
17
|
+
/** @type {WeakMap<object, number>} */
|
|
18
|
+
const IDENTITY_IDS = new WeakMap();
|
|
19
|
+
let nextIdentityId = 1;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A stable per-process id for an identity-compared option value.
|
|
23
|
+
* `null`/`undefined` share id 0 ("absent").
|
|
24
|
+
* @param {any} value
|
|
25
|
+
* @returns {number}
|
|
26
|
+
*/
|
|
27
|
+
export function identityOf(value) {
|
|
28
|
+
if (value == null) return 0;
|
|
29
|
+
let id = IDENTITY_IDS.get(value);
|
|
30
|
+
if (id === undefined) {
|
|
31
|
+
id = nextIdentityId++;
|
|
32
|
+
IDENTITY_IDS.set(value, id);
|
|
33
|
+
}
|
|
34
|
+
return id;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A two-axis compilation cache: documents by identity (weak, entries
|
|
39
|
+
* die with the document), variants per document by derived option key
|
|
40
|
+
* (bounded LRU — the old variants array grew without bound).
|
|
41
|
+
* @param {number} [limitPerDocument] - variant bound per document
|
|
42
|
+
* @returns {{ getOrCompile: (document: object, key: string, compile: () => any) => any }}
|
|
43
|
+
*/
|
|
44
|
+
export function createOptionVariantCache(limitPerDocument = 16) {
|
|
45
|
+
const byDocument = createWeakCache();
|
|
46
|
+
const newVariants = () => createBoundedCache(limitPerDocument);
|
|
47
|
+
return {
|
|
48
|
+
getOrCompile(document, key, compile) {
|
|
49
|
+
const variants = /** @type {import('@jarenjs/core/cache').BoundedCache<string, any>} */ (
|
|
50
|
+
byDocument.getOrCreate(document, newVariants));
|
|
51
|
+
return variants.getOrCreate(key, compile);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|