@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.
Files changed (74) hide show
  1. package/ARCHITECTURE.md +86 -13
  2. package/README.md +248 -23
  3. package/dist/types/canonical.d.ts +37 -0
  4. package/dist/types/cow.d.ts +28 -0
  5. package/dist/types/errors.d.ts +45 -0
  6. package/dist/types/index.d.ts +3 -0
  7. package/dist/types/jslt/errors.d.ts +15 -8
  8. package/dist/types/jslt/index.d.ts +22 -0
  9. package/dist/types/jslt/packs/finance.d.ts +119 -0
  10. package/dist/types/jslt/packs/index.d.ts +310 -0
  11. package/dist/types/jslt/packs/math.d.ts +159 -0
  12. package/dist/types/jslt/packs/stats.d.ts +48 -0
  13. package/dist/types/jslt/registry.d.ts +65 -0
  14. package/dist/types/jtlt/errors.d.ts +3 -6
  15. package/dist/types/option-variants.d.ts +29 -0
  16. package/dist/types/patch.d.ts +214 -0
  17. package/dist/types/path.d.ts +139 -9
  18. package/dist/types/pointer.d.ts +100 -9
  19. package/dist/types/query/compile.d.ts +12 -0
  20. package/dist/types/query/errors.d.ts +72 -8
  21. package/dist/types/query/index.d.ts +317 -25
  22. package/dist/types/query/normalize.d.ts +24 -0
  23. package/dist/types/query/operators.d.ts +241 -1
  24. package/dist/types/query/runtime.d.ts +5 -8
  25. package/dist/types/query/types.d.ts +34 -0
  26. package/dist/types/segments.d.ts +31 -0
  27. package/dist/types/write.d.ts +204 -0
  28. package/dist/types/xquery/parse.d.ts +2 -3
  29. package/docs/JSLT-FORMAT.md +74 -3
  30. package/docs/JSLT-PRELUDE.md +1 -1
  31. package/docs/QUERY-FORMAT.md +695 -33
  32. package/package.json +18 -4
  33. package/schemas/geojson.draft-07.schema.json +323 -0
  34. package/schemas/geojson.jaren.schema.json +863 -0
  35. package/schemas/geojson.schema.json +172 -0
  36. package/schemas/jaren-jslt.authoring.schema.json +142 -0
  37. package/schemas/jaren-jslt.draft-07.schema.json +152 -11
  38. package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
  39. package/schemas/jaren-jslt.schema.json +152 -11
  40. package/schemas/jaren-query.draft-07.schema.json +152 -11
  41. package/schemas/jaren-query.llm-profile.schema.json +619 -0
  42. package/schemas/jaren-query.schema.json +82 -15
  43. package/src/basic.js +1 -1
  44. package/src/canonical.js +170 -0
  45. package/src/cow.js +106 -0
  46. package/src/errors.js +68 -0
  47. package/src/index.js +3 -0
  48. package/src/jslt/dispatch.js +178 -28
  49. package/src/jslt/errors.js +19 -14
  50. package/src/jslt/index.js +37 -29
  51. package/src/jslt/packs/finance.js +49 -0
  52. package/src/jslt/packs/index.js +18 -0
  53. package/src/jslt/packs/math.js +46 -0
  54. package/src/jslt/packs/stats.js +65 -0
  55. package/src/jslt/registry.js +200 -0
  56. package/src/jslt/stylesheet.js +14 -23
  57. package/src/jtlt/desugar.js +2 -3
  58. package/src/jtlt/errors.js +6 -12
  59. package/src/jtlt/index.js +12 -29
  60. package/src/jtlt/template.js +9 -18
  61. package/src/option-variants.js +54 -0
  62. package/src/patch.js +1052 -0
  63. package/src/path.js +319 -52
  64. package/src/pointer.js +225 -44
  65. package/src/query/compile.js +790 -75
  66. package/src/query/errors.js +72 -12
  67. package/src/query/index.js +274 -42
  68. package/src/query/normalize.js +489 -78
  69. package/src/query/operators.js +620 -23
  70. package/src/query/runtime.js +5 -19
  71. package/src/query/types.js +213 -0
  72. package/src/segments.js +409 -64
  73. package/src/write.js +660 -0
  74. package/src/xquery/parse.js +37 -53
package/src/pointer.js CHANGED
@@ -1,13 +1,21 @@
1
1
  //#region JSON Pointer (RFC 6901) + Relative JSON Pointer
2
2
  // JSON Pointer: https://datatracker.ietf.org/doc/html/rfc6901
3
- // Relative JSON Pointer:
4
- // https://datatracker.ietf.org/doc/html/draft-luff-relative-json-pointer-00
3
+ // Relative JSON Pointer: draft-handrews-relative-json-pointer-01, the
4
+ // revision JSON Schema 2020-12 normatively references:
5
+ // https://datatracker.ietf.org/doc/html/draft-handrews-relative-json-pointer-01
6
+ //
7
+ // The newer draft-bhutton-relative-json-pointer-00 adds an index-
8
+ // manipulation form (`0+1`, `1-1`) that this grammar deliberately does not
9
+ // accept: the official format suite still asserts `+1/foo/bar` INVALID, so
10
+ // accepting it would have to arrive together with a dialect gate for the
11
+ // `relative-json-pointer` format tester.
5
12
  //
6
13
  // This module implements JSON Pointer as a two-stage compiler, mirroring
7
14
  // the JSONPath engine in path.js:
8
15
  //
9
16
  // 1. `parseJSONPointer` / `parseRelativeJSONPointer` - strict, single-pass
10
- // char-code parsers enforcing the full RFC 6901 / draft-luff grammar.
17
+ // char-code parsers enforcing the full RFC 6901 / relative-pointer
18
+ // grammar.
11
19
  // 2. `compileJSONPointer` / `compileRelativeJSONPointer` /
12
20
  // `compileDataRef` - compile the parsed form into specialized getter
13
21
  // closures. All decisions (member name decoding, array index parsing,
@@ -24,7 +32,8 @@ import {
24
32
  isDigitCode,
25
33
  } from '@jarenjs/core/scan';
26
34
 
27
- import { NOTHING } from './segments.js';
35
+ import { NOTHING, scanArrayIndex } from './segments.js';
36
+ import { LabeledSyntaxError } from './errors.js';
28
37
 
29
38
  /**
30
39
  * Sentinel for the absence of a value, as distinct from the JSON value
@@ -39,12 +48,9 @@ const hasOwn = Object.hasOwn;
39
48
  * Error thrown when a (relative) JSON Pointer is not valid RFC 6901 /
40
49
  * draft-luff-relative-json-pointer syntax.
41
50
  */
42
- export class JSONPointerSyntaxError extends SyntaxError {
51
+ export class JSONPointerSyntaxError extends LabeledSyntaxError {
43
52
  constructor(message, source, position) {
44
- super(`Invalid JSON Pointer: ${message} at position ${position} in '${source}'`);
45
- this.name = 'JSONPointerSyntaxError';
46
- this.source = source;
47
- this.position = position;
53
+ super('JSONPointerSyntaxError', 'JSON Pointer', message, source, position);
48
54
  }
49
55
  }
50
56
 
@@ -171,38 +177,99 @@ export function parseRelativeJSONPointer(pointer) {
171
177
  return { levels, hash: false, segments: scanSegments(pointer, pos) };
172
178
  }
173
179
 
174
- //#endregion
180
+ const RE_TILDE = /~/g;
181
+ const RE_SLASH = /\//g;
182
+ const RE_ESCAPED_SLASH = /~1/g;
183
+ const RE_ESCAPED_TILDE = /~0/g;
175
184
 
176
- //#region compiler
185
+ /**
186
+ * Encode a single reference token for use inside an RFC 6901 JSON
187
+ * Pointer: `~` becomes `~0` and `/` becomes `~1` (RFC 6901 section 3).
188
+ * The write-side inverse of the parser's decode; the escape-free common
189
+ * case returns the input unchanged.
190
+ * @param {string|number} segment - The raw member name or array index
191
+ * @returns {string} The encoded reference token
192
+ * @example
193
+ * encodeJSONPointerSegment('a/b'); // 'a~1b'
194
+ */
195
+ export function encodeJSONPointerSegment(segment) {
196
+ const s = String(segment);
197
+ return (s.indexOf('~') < 0 && s.indexOf('/') < 0)
198
+ ? s
199
+ : s.replace(RE_TILDE, '~0').replace(RE_SLASH, '~1');
200
+ }
177
201
 
178
- // Array indexes are bounded by the maximum array length (2^32 - 1), so a
179
- // valid index has at most 10 digits and is strictly below 2^32 - 1.
180
- const MAX_ARRAY_INDEX = 4294967294;
202
+ /**
203
+ * Decode one RFC 6901 reference token: `~1` becomes `/` and `~0` becomes
204
+ * `~`. The order is normative (RFC 6901 section 4) and not an accident of
205
+ * implementation — decoding `~0` first would turn `~01` into `~1` and then
206
+ * into `/`, so a member literally named `~1` would come back as `/`.
207
+ * The escape-free common case returns the input unchanged.
208
+ * @param {string} token - One encoded reference token (no `/` separators)
209
+ * @returns {string} The decoded member name
210
+ * @example
211
+ * decodeJSONPointerSegment('a~1b'); // 'a/b'
212
+ */
213
+ export function decodeJSONPointerSegment(token) {
214
+ return token.indexOf('~') < 0
215
+ ? token
216
+ : token.replace(RE_ESCAPED_SLASH, '/').replace(RE_ESCAPED_TILDE, '~');
217
+ }
181
218
 
182
219
  /**
183
- * Scan `source[start..end)` as an RFC 6901 array index: `0`, or a digit
184
- * sequence without leading zeros. Returns -1 when the range is not a
185
- * valid index (`-` is never a valid read index).
220
+ * Format decoded reference tokens as an RFC 6901 JSON Pointer; the
221
+ * inverse of `parseJSONPointer`. An empty array formats as the empty
222
+ * (whole-document) pointer.
223
+ * @param {(string|number)[]} segments - Decoded reference tokens
224
+ * @returns {string} The JSON Pointer
225
+ * @example
226
+ * formatJSONPointer(['a/b', 0]); // '/a~1b/0'
186
227
  */
187
- function scanArrayIndex(source, start, end) {
188
- const digits = end - start;
189
- if (digits === 0 || digits > 10)
190
- return -1;
191
- const first = source.charCodeAt(start);
192
- if (!isDigitCode(first))
193
- return -1;
194
- if (first === CC_0)
195
- return digits === 1 ? 0 : -1;
196
- let index = first - CC_0;
197
- for (let i = start + 1; i < end; i++) {
198
- const c = source.charCodeAt(i);
199
- if (!isDigitCode(c))
200
- return -1;
201
- index = index * 10 + (c - CC_0);
228
+ export function formatJSONPointer(segments) {
229
+ let out = '';
230
+ for (let i = 0; i < segments.length; i++)
231
+ out += '/' + encodeJSONPointerSegment(segments[i]);
232
+ return out;
233
+ }
234
+
235
+ /**
236
+ * Parse a JSON Pointer into a path array, narrowing every canonical array
237
+ * index to a number and leaving all other tokens as decoded strings.
238
+ *
239
+ * RFC 6901 has no types: `/items/0/id` addresses element 0 of an array and
240
+ * member `"0"` of an object with the same token. This function resolves that
241
+ * lexically, not against a document — a token is a number when it is `0` or a
242
+ * digit sequence without leading zeros within the array-index range, which is
243
+ * the same rule the pointer compiler uses to pre-parse indexes. `"01"`,
244
+ * `"1e0"`, `"-"` and `"1abc"` therefore stay strings.
245
+ *
246
+ * The result is the `(string|number)[]` shape that error reporters and
247
+ * diffing tools use for data locations, so it pairs directly with a
248
+ * validation error's `instancePath`. `formatJSONPointer` is the inverse.
249
+ * @param {string} pointer - The JSON Pointer (e.g. `/store/book/0`)
250
+ * @returns {(string|number)[]} Decoded tokens with array indexes as numbers
251
+ * @throws {JSONPointerSyntaxError} When the pointer violates the grammar
252
+ * @example
253
+ * parseJSONPointerPath('/items/0/id'); // ['items', 0, 'id']
254
+ * parseJSONPointerPath('/items/01'); // ['items', '01']
255
+ * parseJSONPointerPath('/a~1b'); // ['a/b']
256
+ */
257
+ export function parseJSONPointerPath(pointer) {
258
+ const segments = parseJSONPointer(pointer);
259
+ /** @type {(string|number)[]} */
260
+ const path = segments;
261
+ for (let i = 0; i < segments.length; i++) {
262
+ const token = segments[i];
263
+ const index = scanArrayIndex(token, 0, token.length);
264
+ if (index !== -1) path[i] = index;
202
265
  }
203
- return index <= MAX_ARRAY_INDEX ? index : -1;
266
+ return path;
204
267
  }
205
268
 
269
+ //#endregion
270
+
271
+ //#region compiler
272
+
206
273
  /**
207
274
  * One pointer hop: an array is addressed by the pre-parsed index, an
208
275
  * object by the pre-decoded member name (own properties only), anything
@@ -247,6 +314,28 @@ function compileSegmentsGetter(segments) {
247
314
  return v === NOTHING ? NOTHING : hop(v, name1, index1);
248
315
  };
249
316
  }
317
+ const name2 = segments[2];
318
+ const index2 = scanArrayIndex(name2, 0, name2.length);
319
+ if (slen === 3) {
320
+ return function pointerGetter3(root) {
321
+ const v0 = hop(root, name0, index0);
322
+ if (v0 === NOTHING) return NOTHING;
323
+ const v1 = hop(v0, name1, index1);
324
+ return v1 === NOTHING ? NOTHING : hop(v1, name2, index2);
325
+ };
326
+ }
327
+ const name3 = segments[3];
328
+ const index3 = scanArrayIndex(name3, 0, name3.length);
329
+ if (slen === 4) {
330
+ return function pointerGetter4(root) {
331
+ const v0 = hop(root, name0, index0);
332
+ if (v0 === NOTHING) return NOTHING;
333
+ const v1 = hop(v0, name1, index1);
334
+ if (v1 === NOTHING) return NOTHING;
335
+ const v2 = hop(v1, name2, index2);
336
+ return v2 === NOTHING ? NOTHING : hop(v2, name3, index3);
337
+ };
338
+ }
250
339
  const names = segments;
251
340
  const indexes = new Array(slen);
252
341
  for (let i = 0; i < slen; i++)
@@ -326,11 +415,15 @@ function decodeSegmentRange(source, start, end, tilde) {
326
415
  /**
327
416
  * The last segment of `dataPath.slice(0, end)`, decoded lazily: the
328
417
  * common escape-free case allocates nothing beyond the result slice.
329
- * At the root (`end === 0`) the name of the location is `''`.
418
+ *
419
+ * The root (`end === 0`) has no name, so it yields NOTHING rather than
420
+ * `''` — `''` is a member name a document can genuinely have (`{"": 1}`
421
+ * at `/`), and returning it for the root too would make the two
422
+ * indistinguishable to the caller.
330
423
  */
331
424
  function lastSegmentOf(dataPath, end) {
332
425
  if (end === 0)
333
- return '';
426
+ return NOTHING;
334
427
  const start = dataPath.lastIndexOf('/', end - 1) + 1;
335
428
  for (let i = start; i < end; i++) {
336
429
  if (dataPath.charCodeAt(i) === CC_TILDE)
@@ -339,6 +432,34 @@ function lastSegmentOf(dataPath, end) {
339
432
  return dataPath.slice(start, end);
340
433
  }
341
434
 
435
+ /**
436
+ * Read and validate the `hashIndex` compile option, defaulting to the
437
+ * historical `'string'`. An unknown value is rejected rather than ignored:
438
+ * silently falling back would hand a caller who meant `'number'` the exact
439
+ * behavior they were opting out of.
440
+ * @param {{ hashIndex?: string }} [options]
441
+ * @returns {'string'|'number'}
442
+ */
443
+ function readHashIndexOption(options) {
444
+ if (options === undefined || options === null)
445
+ return 'string';
446
+ const mode = options.hashIndex;
447
+ if (mode === undefined || mode === 'string')
448
+ return 'string';
449
+ if (mode === 'number')
450
+ return 'number';
451
+ throw new TypeError(
452
+ `hashIndex must be 'string' or 'number', got ${JSON.stringify(mode)}`);
453
+ }
454
+
455
+ /**
456
+ * The exclusive end of the *parent* of the location `dataPath.slice(0, end)`,
457
+ * or -1 when that location is the root and so has no parent.
458
+ */
459
+ function parentEndOf(dataPath, end) {
460
+ return end === 0 ? -1 : dataPath.lastIndexOf('/', end - 1);
461
+ }
462
+
342
463
  /**
343
464
  * Walk `root` along the location path prefix `path.slice(0, end)`.
344
465
  * Segments are decoded lazily per hop (escape-free segments are sliced
@@ -382,23 +503,65 @@ function walkPointerPrefix(root, path, end) {
382
503
  *
383
504
  * The relative part (level count, `#` form, trailing segments) compiles
384
505
  * once; per call only `dataPath` - the current location in `dataRoot` as
385
- * an RFC 6901 pointer - varies. The `#` form resolves to the member name
386
- * or array index of the location **as a string** (`''` at the root),
387
- * matching the historical behavior relied on by the validator's `$data`
388
- * keyword.
506
+ * an RFC 6901 pointer - varies.
507
+ *
508
+ * The root has no name: `0#` there yields `JSONPOINTER_NOTHING`, not `''`,
509
+ * so it stays distinguishable from the member named `''` (`{"": 1}` at
510
+ * `/`), which is a name a document can genuinely have.
511
+ *
512
+ * ### The `#` form and `hashIndex`
513
+ *
514
+ * Relative JSON Pointer says `#` yields the member *name* for an object
515
+ * member and the *index* — a number — for an array element. Telling those
516
+ * apart requires looking at the container, so the two modes cost different
517
+ * things and you choose per compile:
518
+ *
519
+ * - `hashIndex: 'string'` (**default**) answers from `dataPath` alone and
520
+ * never touches `dataRoot`: an array position comes back as the string
521
+ * `'1'`. This is the historical behavior the validator's `$data` keyword
522
+ * relies on, and it is a string operation — tens of nanoseconds.
523
+ * - `hashIndex: 'number'` is the draft's answer. It walks to the parent of
524
+ * the location to see whether it is an array, and returns `1` rather than
525
+ * `'1'` when it is. Object member names are unaffected. When the parent
526
+ * cannot be reached (the location does not exist in `dataRoot`) it falls
527
+ * back to the string, because nothing proves the position is an index.
528
+ *
529
+ * Neither mode verifies that the location itself exists; the caller is
530
+ * expected to pass a location it actually reached. The non-`#` form must
531
+ * walk regardless, because it returns the value.
389
532
  *
390
533
  * @param {string} pointer - The relative pointer (e.g. `1/sibling`, `0#`)
534
+ * @param {{ hashIndex?: 'string'|'number' }} [options] - `hashIndex`
535
+ * selects what the `#` form yields for an array position (default
536
+ * `'string'`)
391
537
  * @returns {RelativeJsonPointerResolver} resolver returning
392
538
  * the addressed value, or `JSONPOINTER_NOTHING`
393
539
  * @throws {JSONPointerSyntaxError} When the pointer is not valid
540
+ * @throws {TypeError} When `hashIndex` is neither `'string'` nor `'number'`
394
541
  * @example
395
542
  * const resolve = compileRelativeJSONPointer('1/limits');
396
543
  * resolve({ limits: { min: 2 } , value: 5 }, '/value'); // { min: 2 }
544
+ * @example
545
+ * const spec = compileRelativeJSONPointer('0#', { hashIndex: 'number' });
546
+ * spec({ a: ['x', 'y'] }, '/a/1'); // 1 (the number, per the draft)
397
547
  */
398
- export function compileRelativeJSONPointer(pointer) {
548
+ export function compileRelativeJSONPointer(pointer, options = undefined) {
399
549
  const { levels, hash, segments } = parseRelativeJSONPointer(pointer);
550
+ const numericHash = readHashIndexOption(options) === 'number';
400
551
  if (hash) {
401
- return function relativeHashResolver(dataRoot, dataPath) {
552
+ if (!numericHash) {
553
+ return function relativeHashResolver(dataRoot, dataPath) {
554
+ if (typeof dataPath !== 'string')
555
+ dataPath = '';
556
+ else if (dataPath.length !== 0 && dataPath.charCodeAt(0) !== CC_SLASH)
557
+ return NOTHING;
558
+ const end = trimLevels(dataPath, levels);
559
+ if (end < 0)
560
+ return NOTHING;
561
+ return lastSegmentOf(dataPath, end);
562
+ };
563
+ }
564
+ return function relativeHashIndexResolver(dataRoot, dataPath) {
402
565
  if (typeof dataPath !== 'string')
403
566
  dataPath = '';
404
567
  else if (dataPath.length !== 0 && dataPath.charCodeAt(0) !== CC_SLASH)
@@ -406,7 +569,19 @@ export function compileRelativeJSONPointer(pointer) {
406
569
  const end = trimLevels(dataPath, levels);
407
570
  if (end < 0)
408
571
  return NOTHING;
409
- return lastSegmentOf(dataPath, end);
572
+ const name = lastSegmentOf(dataPath, end);
573
+ if (name === NOTHING)
574
+ return NOTHING;
575
+ // An index is only an index when its container is an array; the name
576
+ // of a `{"1": …}` member is the string "1" in every mode.
577
+ const parentEnd = parentEndOf(dataPath, end);
578
+ if (parentEnd < 0)
579
+ return name;
580
+ const parent = walkPointerPrefix(dataRoot, dataPath, parentEnd);
581
+ if (!Array.isArray(parent))
582
+ return name;
583
+ const index = scanArrayIndex(name, 0, name.length);
584
+ return index < 0 ? name : index;
410
585
  };
411
586
  }
412
587
  const getter = compileSegmentsGetter(segments);
@@ -430,19 +605,25 @@ export function compileRelativeJSONPointer(pointer) {
430
605
  * Pointer, a leading `/` an absolute JSON Pointer, and `''` the root.
431
606
  *
432
607
  * @param {string} ref - The reference string
608
+ * @param {{ hashIndex?: 'string'|'number' }} [options] - forwarded to
609
+ * {@link compileRelativeJSONPointer}; only the relative forms read it
433
610
  * @returns {RelativeJsonPointerResolver} resolver returning
434
611
  * the addressed value, or `JSONPOINTER_NOTHING`
435
612
  * @throws {JSONPointerSyntaxError} When the reference is none of the
436
613
  * accepted forms
614
+ * @throws {TypeError} When `hashIndex` is neither `'string'` nor `'number'`
437
615
  */
438
- export function compileDataRef(ref) {
616
+ export function compileDataRef(ref, options = undefined) {
439
617
  if (typeof ref !== 'string')
440
618
  throw new JSONPointerSyntaxError('a data reference must be a string', String(ref), 0);
619
+ // Validate the option even on the forms that ignore it, so a typo is a
620
+ // compile-time error wherever it appears rather than only on `N#` refs.
621
+ readHashIndexOption(options);
441
622
  if (ref.length === 0)
442
623
  return getRoot;
443
624
  const c = ref.charCodeAt(0);
444
625
  if (isDigitCode(c))
445
- return compileRelativeJSONPointer(ref);
626
+ return compileRelativeJSONPointer(ref, options);
446
627
  if (c === CC_SLASH)
447
628
  return compileSegmentsGetter(scanSegments(ref, 0));
448
629
  throw new JSONPointerSyntaxError('a data reference must be empty, a JSON Pointer or a Relative JSON Pointer', ref, 0);