@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/path.js CHANGED
@@ -16,7 +16,10 @@
16
16
  // - Singular queries (`$.a.b[3]`) compile to a direct property walk with
17
17
  // no intermediate arrays.
18
18
  // - Filter comparables compile to sentinel-returning getters; existence
19
- // tests on singular queries never materialize nodelists.
19
+ // tests on singular queries never materialize nodelists. A `$`-rooted
20
+ // comparable is memoized per filter application (its value is invariant
21
+ // across the candidates), with the memo reset before each application so
22
+ // a mutated document re-queried under the same root reads fresh.
20
23
  // - `match()`/`search()` with a literal pattern precompile their RegExp;
21
24
  // dynamic patterns use a per-callsite monomorphic cache.
22
25
  // - Normalized-path production (RFC 9535 section 2.7) is compiled lazily,
@@ -30,9 +33,17 @@ import {
30
33
  compileSingularGetter,
31
34
  compileSegmentV,
32
35
  runSegmentsV,
36
+ compileSegmentG,
37
+ runSegmentsG,
33
38
  compileSegmentP,
34
39
  runSegmentsP,
40
+ scanArrayIndex,
41
+ appendName,
35
42
  } from './segments.js';
43
+ import {
44
+ parseJSONPointer,
45
+ encodeJSONPointerSegment,
46
+ } from './pointer.js';
36
47
  import {
37
48
  CC_TAB,
38
49
  CC_LF,
@@ -64,7 +75,12 @@ import {
64
75
  CC_UNDERSCORE,
65
76
  CC_PIPE,
66
77
  isDigitCode,
78
+ isNameStartCode,
79
+ isNameCharCode,
67
80
  } from '@jarenjs/core/scan';
81
+ import { deepFreeze } from '@jarenjs/core/object';
82
+ import { createBoundedCache, createWeakCache } from '@jarenjs/core/cache';
83
+ import { LabeledSyntaxError } from './errors.js';
68
84
 
69
85
  /**
70
86
  * Sentinel for the absence of a value ("Nothing" in RFC 9535 terms), as
@@ -72,32 +88,23 @@ import {
72
88
  */
73
89
  export const JSONPATH_NOTHING = NOTHING;
74
90
 
75
- function deepFreeze(value) {
76
- if (typeof value !== 'object' || value === null)
77
- return value;
78
- const keys = Object.keys(value);
79
- for (let i = 0; i < keys.length; i++)
80
- deepFreeze(value[keys[i]]);
81
- return Object.freeze(value);
82
- }
83
-
84
91
  /**
85
92
  * Error thrown when a JSONPath query is not valid RFC 9535 syntax
86
93
  * (including queries that are not well-typed per section 2.4.3).
87
94
  */
88
- export class JSONPathSyntaxError extends SyntaxError {
95
+ export class JSONPathSyntaxError extends LabeledSyntaxError {
89
96
  constructor(message, source, position) {
90
- super(`Invalid JSONPath: ${message} at position ${position} in '${source}'`);
91
- this.name = 'JSONPathSyntaxError';
92
- this.source = source;
93
- this.position = position;
97
+ super('JSONPathSyntaxError', 'JSONPath', message, source, position);
94
98
  }
95
99
  }
96
100
 
97
101
  // Built-in function extensions (RFC 9535 section 2.4).
98
102
  // Parameter/return types: 'value' = ValueType, 'nodes' = NodesType,
99
- // 'logical' = LogicalType.
103
+ // 'logical' = LogicalType. Null-prototype, so an inherited member name
104
+ // (`constructor`, `tostring`) is an unknown function and not a
105
+ // half-formed descriptor.
100
106
  const FUNCTIONS = {
107
+ __proto__: null,
101
108
  length: { params: ['value'], returns: 'value' },
102
109
  count: { params: ['nodes'], returns: 'value' },
103
110
  match: { params: ['value', 'value'], returns: 'logical' },
@@ -105,21 +112,83 @@ const FUNCTIONS = {
105
112
  value: { params: ['nodes'], returns: 'value' },
106
113
  };
107
114
 
108
- //#endregion
115
+ // function-name = LCALPHA *(LCALPHA / "_" / DIGIT) (RFC 9535 2.4.1)
116
+ const RE_FUNCTION_NAME = /^[a-z][a-z0-9_]*$/;
117
+ const TYPE_NAMES = ['value', 'nodes', 'logical'];
109
118
 
110
- //#region parser
119
+ // Registries are validated once and memoized on the registry object, so
120
+ // compiling a thousand queries against one registry validates it once.
121
+ const FUNCTION_TABLES = new WeakMap();
111
122
 
112
- function isNameFirstCode(c) {
113
- return (c >= 0x41 && c <= 0x5A) // A-Z
114
- || (c >= 0x61 && c <= 0x7A) // a-z
115
- || c === CC_UNDERSCORE
116
- || c >= 0x80; // any non-ASCII code unit
123
+ function registryError(message) {
124
+ return new TypeError(`options.pathFunctions: ${message}`);
117
125
  }
118
126
 
119
- function isNameCharCode(c) {
120
- return isNameFirstCode(c) || isDigitCode(c);
127
+ function validateType(type, what) {
128
+ if (!TYPE_NAMES.includes(type))
129
+ throw registryError(`${what} must be one of ${TYPE_NAMES.join(', ')}`);
121
130
  }
122
131
 
132
+ /**
133
+ * Validate a registry of custom function extensions and merge it over
134
+ * the built-ins. A registry entry declares its parameter and result
135
+ * types exactly like a built-in, which is what lets the parser apply
136
+ * the same well-typedness rules (RFC 9535 section 2.4.3) to it.
137
+ * @param {object} functions - name to `{ params, returns, evaluate }`
138
+ * @returns {object} the merged, null-prototype function table
139
+ */
140
+ function resolveFunctionTable(functions) {
141
+ const cached = FUNCTION_TABLES.get(functions);
142
+ if (cached !== undefined)
143
+ return cached;
144
+ const table = { __proto__: null, ...FUNCTIONS };
145
+ for (const name of Object.keys(functions)) {
146
+ const def = functions[name];
147
+ if (!RE_FUNCTION_NAME.test(name))
148
+ throw registryError(`'${name}' is not a valid function name`);
149
+ // section 2.4.1: an extension must not redefine a built-in, and a
150
+ // literal keyword would be unreadable as a call
151
+ if (FUNCTIONS[name] !== undefined)
152
+ throw registryError(`'${name}' is a built-in function`);
153
+ if (name === 'true' || name === 'false' || name === 'null')
154
+ throw registryError(`'${name}' is a literal, not a function name`);
155
+ if (def === null || typeof def !== 'object')
156
+ throw registryError(`'${name}' must be a { params, returns, evaluate } object`);
157
+ if (!Array.isArray(def.params))
158
+ throw registryError(`'${name}'.params must be an array of parameter types`);
159
+ for (let i = 0; i < def.params.length; i++)
160
+ validateType(def.params[i], `'${name}'.params[${i}]`);
161
+ validateType(def.returns, `'${name}'.returns`);
162
+ if (typeof def.evaluate !== 'function')
163
+ throw registryError(`'${name}'.evaluate must be a function`);
164
+ table[name] = {
165
+ params: [...def.params],
166
+ returns: def.returns,
167
+ evaluate: def.evaluate,
168
+ };
169
+ }
170
+ Object.freeze(table);
171
+ FUNCTION_TABLES.set(functions, table);
172
+ return table;
173
+ }
174
+
175
+ // Resolve the function table a parse should use. The overwhelmingly
176
+ // common call has no options at all and reaches the built-ins directly.
177
+ function functionTableOf(options) {
178
+ if (options == null)
179
+ return FUNCTIONS;
180
+ const functions = options.pathFunctions;
181
+ if (functions == null)
182
+ return FUNCTIONS;
183
+ if (typeof functions !== 'object' || Array.isArray(functions))
184
+ throw registryError('must be a plain object of function extensions');
185
+ return resolveFunctionTable(functions);
186
+ }
187
+
188
+ //#endregion
189
+
190
+ //#region parser
191
+
123
192
  /**
124
193
  * Selects a named object member (RFC 9535 name selector).
125
194
  * @typedef {Object} JSONPathNameSelector
@@ -196,6 +265,7 @@ function isNameCharCode(c) {
196
265
  * values: (data: any) => any[],
197
266
  * first: (data: any) => any,
198
267
  * exists: (data: any) => boolean,
268
+ * iterate: (data: any) => Generator<any>,
199
269
  * nodes: (data: any) => JSONPathNode[],
200
270
  * paths: (data: any) => string[],
201
271
  * source: string,
@@ -203,16 +273,43 @@ function isNameCharCode(c) {
203
273
  * }} JSONPathQuery
204
274
  */
205
275
 
276
+ /**
277
+ * A custom JSONPath function extension (RFC 9535 section 2.4). The
278
+ * declared types are what the parser type-checks call sites against
279
+ * (section 2.4.3), exactly as it does for the five built-ins.
280
+ *
281
+ * `evaluate` receives one argument per declared parameter: a `value`
282
+ * parameter arrives as a JSON value or `JSONPATH_NOTHING`, a `nodes`
283
+ * parameter as an array of the selected values, a `logical` parameter
284
+ * as a boolean. Its result must match `returns` - a `value` function
285
+ * may return `undefined` to mean Nothing.
286
+ * @typedef {Object} JSONPathFunction
287
+ * @property {('value'|'nodes'|'logical')[]} params - Declared parameter types
288
+ * @property {'value'|'nodes'|'logical'} returns - Declared result type
289
+ * @property {(...args: any[]) => any} evaluate - The implementation
290
+ */
291
+
292
+ /**
293
+ * Options accepted by the JSONPath entry points.
294
+ * @typedef {Object} JSONPathOptions
295
+ * @property {Record<string, JSONPathFunction>} [pathFunctions] - Custom
296
+ * function extensions, by name. A name must match the RFC's
297
+ * `function-name` production and must not redefine a built-in.
298
+ */
299
+
206
300
  /**
207
301
  * Parse a JSONPath query string into an AST.
208
302
  * @param {string} source - The JSONPath expression (e.g. `$.store.book[?@.price < 10].title`)
303
+ * @param {JSONPathOptions} [options] - Parse options
209
304
  * @returns {JSONPathAst} The parsed query AST
210
305
  * @throws {JSONPathSyntaxError} When the query violates the RFC 9535 grammar
306
+ * @throws {TypeError} When `options.pathFunctions` is not a valid registry
211
307
  */
212
- export function parseJSONPath(source) {
308
+ export function parseJSONPath(source, options = undefined) {
213
309
  if (typeof source !== 'string')
214
310
  throw new JSONPathSyntaxError('query must be a string', String(source), 0);
215
311
 
312
+ const FUNCS = functionTableOf(options);
216
313
  const len = source.length;
217
314
  let pos = 0;
218
315
 
@@ -247,7 +344,7 @@ export function parseJSONPath(source) {
247
344
 
248
345
  function parseMemberNameShorthand() {
249
346
  const start = pos;
250
- if (!isNameFirstCode(cc(pos)))
347
+ if (!isNameStartCode(cc(pos)))
251
348
  fail('expected member name');
252
349
  while (pos < len) {
253
350
  const c = source.charCodeAt(pos);
@@ -779,7 +876,7 @@ export function parseJSONPath(source) {
779
876
  }
780
877
 
781
878
  function parseFunctionExpr(name, at) {
782
- const def = FUNCTIONS[name];
879
+ const def = FUNCS[name];
783
880
  if (def === undefined)
784
881
  fail(`unknown function '${name}'`, at);
785
882
  pos++; // consume '('
@@ -787,7 +884,12 @@ export function parseJSONPath(source) {
787
884
  const args = [];
788
885
  if (cc(pos) !== CC_RPAREN) {
789
886
  for (;;) {
790
- args.push(parseFunctionArg());
887
+ // argument parsing is type-directed: a LogicalType parameter
888
+ // takes the whole logical-expr production (RFC 9535 2.4.3), so
889
+ // `!`, `(`, comparisons and `&&`/`||` are only legal there
890
+ args.push(def.params[args.length] === 'logical'
891
+ ? { kind: 'logical', expr: parseLogicalOr() }
892
+ : parseFunctionArg());
791
893
  skipWS();
792
894
  if (cc(pos) === CC_COMMA) {
793
895
  pos++;
@@ -816,15 +918,22 @@ export function parseJSONPath(source) {
816
918
  continue;
817
919
  fail(`argument ${i + 1} of '${name}' must be of type ValueType`, at);
818
920
  }
819
- else { // 'nodes'
921
+ else if (param === 'nodes') {
820
922
  if (arg.kind === 'query')
821
923
  continue;
822
924
  if (arg.kind === 'func' && arg.returns === 'nodes')
823
925
  continue;
824
926
  fail(`argument ${i + 1} of '${name}' must be a query (NodesType)`, at);
825
927
  }
928
+ // 'logical': parseLogicalOr only produces well-typed logical
929
+ // expressions, and it already rejects a ValueType function there
930
+ }
931
+ const node = { kind: 'func', name, args, returns: def.returns };
932
+ if (def.evaluate !== undefined) {
933
+ node.params = def.params;
934
+ node.evaluate = def.evaluate;
826
935
  }
827
- return { kind: 'func', name, args, returns: def.returns };
936
+ return node;
828
937
  }
829
938
 
830
939
  //#endregion
@@ -860,6 +969,9 @@ export function parseJSONPath(source) {
860
969
  * - `query(data)` / `query.values(data)` - array of matched values
861
970
  * - `query.first(data)` - first matched value, or `undefined`
862
971
  * - `query.exists(data)` - true when the query selects at least one node
972
+ * - `query.iterate(data)` - a generator yielding matched values on
973
+ * demand, in document order; `first`/`exists` are one pull of it, so
974
+ * none of the three builds a nodelist it does not need
863
975
  * - `query.nodes(data)` - array of `{ path, value }` with normalized paths
864
976
  * - `query.paths(data)` - array of normalized paths (RFC 9535 section 2.7)
865
977
  * - `query.source` - the original query string
@@ -867,18 +979,20 @@ export function parseJSONPath(source) {
867
979
  * compiled path mode must agree with the eagerly compiled value mode)
868
980
  *
869
981
  * @param {string} source - The JSONPath expression
982
+ * @param {JSONPathOptions} [options] - Compile options
870
983
  * @returns {JSONPathQuery} The compiled query function
871
984
  * @throws {JSONPathSyntaxError} When the query is not valid RFC 9535
985
+ * @throws {TypeError} When `options.pathFunctions` is not a valid registry
872
986
  * @example
873
987
  * const q = compileJSONPath('$.store.book[?@.price < 10].title');
874
988
  * q(data); // ['Sayings of the Century', 'Moby Dick']
875
989
  * q.paths(data); // ["$['store']['book'][0]['title']", ...]
876
990
  */
877
- export function compileJSONPath(source) {
878
- const ast = deepFreeze(parseJSONPath(source));
991
+ export function compileJSONPath(source, options = undefined) {
992
+ const ast = deepFreeze(parseJSONPath(source, options));
879
993
  const segments = ast.segments;
880
994
 
881
- let values, first, exists;
995
+ let values, first, exists, iterate;
882
996
  if (isSingularSegments(segments)) {
883
997
  const getter = compileSingularGetter(segments, false);
884
998
  values = (data) => {
@@ -890,15 +1004,29 @@ export function compileJSONPath(source) {
890
1004
  return v === NOTHING ? undefined : v;
891
1005
  };
892
1006
  exists = (data) => getter(data, data) !== NOTHING;
1007
+ iterate = function* iterateSingular(data) {
1008
+ const v = getter(data, data);
1009
+ if (v !== NOTHING)
1010
+ yield v;
1011
+ };
893
1012
  }
894
1013
  else {
895
1014
  const segs = segments.map(compileSegmentV);
896
1015
  values = (data) => runSegmentsV(segs, data, data);
1016
+ // the lazy chain is a second compilation of the same selectors, so
1017
+ // it is built on first use - a query that only ever calls values()
1018
+ // never pays for it
1019
+ let gens = null;
1020
+ iterate = (data) => {
1021
+ if (gens === null)
1022
+ gens = segments.map(compileSegmentG);
1023
+ return runSegmentsG(gens, 0, data, data);
1024
+ };
897
1025
  first = (data) => {
898
- const result = runSegmentsV(segs, data, data);
899
- return result.length !== 0 ? result[0] : undefined;
1026
+ const r = iterate(data).next();
1027
+ return r.done ? undefined : r.value;
900
1028
  };
901
- exists = (data) => runSegmentsV(segs, data, data).length !== 0;
1029
+ exists = (data) => !iterate(data).next().done;
902
1030
  }
903
1031
 
904
1032
  // nodes mode is compiled lazily; value-only queries never pay for it
@@ -913,6 +1041,7 @@ export function compileJSONPath(source) {
913
1041
  query.values = values;
914
1042
  query.first = first;
915
1043
  query.exists = exists;
1044
+ query.iterate = iterate;
916
1045
  query.nodes = (data) => {
917
1046
  const { vals, paths } = runNodes(data);
918
1047
  const nodes = new Array(vals.length);
@@ -926,26 +1055,31 @@ export function compileJSONPath(source) {
926
1055
  return query;
927
1056
  }
928
1057
 
929
- const QUERY_CACHE = new Map();
930
- const QUERY_CACHE_LIMIT = 512;
1058
+ const QUERY_CACHE = createBoundedCache(512);
1059
+ // One cache per registry, because the same source compiles differently
1060
+ // under different extensions; keyed weakly so a registry that goes out
1061
+ // of scope takes its compiled queries with it. A WeakMap of bounded
1062
+ // caches — the two axes of `@jarenjs/core/cache`, composed.
1063
+ const REGISTRY_CACHES = createWeakCache();
1064
+ const boundedCacheFor = () => createBoundedCache(512);
931
1065
 
932
1066
  /**
933
1067
  * Apply a JSONPath query to a JSON value in one call. Compiled queries
934
- * are cached (FIFO, 512 entries), so repeated calls with the same query
935
- * string reuse the compiled function.
1068
+ * are cached (bounded LRU, 512 entries), so repeated calls with the
1069
+ * same query string reuse the compiled function. A query compiled
1070
+ * against a function-extension registry is cached under that registry.
936
1071
  * @param {string} source - The JSONPath expression
937
1072
  * @param {any} data - The JSON value to query
1073
+ * @param {JSONPathOptions} [options] - Compile options
938
1074
  * @returns {any[]} Array of matched values
939
1075
  * @throws {JSONPathSyntaxError} When the query is not valid RFC 9535
940
1076
  */
941
- export function queryJSONPath(source, data) {
942
- let query = QUERY_CACHE.get(source);
943
- if (query === undefined) {
944
- query = compileJSONPath(source);
945
- if (QUERY_CACHE.size >= QUERY_CACHE_LIMIT)
946
- QUERY_CACHE.delete(QUERY_CACHE.keys().next().value);
947
- QUERY_CACHE.set(source, query);
948
- }
1077
+ export function queryJSONPath(source, data, options = undefined) {
1078
+ const functions = options == null ? null : options.pathFunctions;
1079
+ const cache = functions == null
1080
+ ? QUERY_CACHE
1081
+ : REGISTRY_CACHES.getOrCreate(functions, boundedCacheFor);
1082
+ const query = cache.getOrCreate(source, (src) => compileJSONPath(src, options));
949
1083
  return query(data);
950
1084
  }
951
1085
 
@@ -953,25 +1087,158 @@ export function queryJSONPath(source, data) {
953
1087
  * Validates a JSONPath expression strictly against the RFC 9535 grammar,
954
1088
  * including well-typedness of function expressions. Unlike the heuristic
955
1089
  * `isValidJSONPath` in basic.js, this uses the full parser.
1090
+ *
1091
+ * Without options this recognizes the five built-in functions and
1092
+ * nothing else, which is what the registered `json-path` string format
1093
+ * asserts: a format is a property of the string itself, so it must mean
1094
+ * the same thing in every schema, independent of which extensions some
1095
+ * host happens to have installed. Pass `options.pathFunctions` to
1096
+ * validate against a registry instead - a host that wants its
1097
+ * extensions asserted registers a tester bound to them.
1098
+ *
956
1099
  * @param {string} str - The JSONPath expression to validate
1100
+ * @param {JSONPathOptions} [options] - Compile options
957
1101
  * @returns {boolean} True when the string is a valid RFC 9535 query
958
1102
  * @example
959
1103
  * isValidJSONPathStrict('$.store.book[?@.price < 10]'); // true
960
1104
  * isValidJSONPathStrict('$.store.book[0 5]'); // false
961
1105
  * isValidJSONPathStrict('@.name'); // false (queries start at $)
962
1106
  */
963
- export function isValidJSONPathStrict(str) {
1107
+ export function isValidJSONPathStrict(str, options = undefined) {
964
1108
  if (typeof str !== 'string')
965
1109
  return false;
966
1110
  try {
967
- parseJSONPath(str);
1111
+ parseJSONPath(str, options);
968
1112
  return true;
969
1113
  }
970
- catch {
1114
+ catch (error) {
1115
+ // a malformed registry is the host's bug, not the string's
1116
+ if (!(error instanceof JSONPathSyntaxError))
1117
+ throw error;
971
1118
  return false;
972
1119
  }
973
1120
  }
974
1121
 
1122
+ /**
1123
+ * The head of a variable-rooted path string: `$` followed by a variable
1124
+ * name. The Jaren query format writes a path relative to a bound
1125
+ * variable as `$name` plus ordinary RFC 9535 segments, and the query
1126
+ * normalizer splits on exactly this production - so schema-time
1127
+ * validation and compile-time parsing agree on where the segments start.
1128
+ */
1129
+ export const RE_JSONPATH_VARIABLE_HEAD = /^\$([A-Za-z_][A-Za-z0-9_]*)/;
1130
+
1131
+ /**
1132
+ * Validates a variable-rooted path string: `$name`, optionally followed
1133
+ * by RFC 9535 segments (`$book.price`, `$b[?@.isbn]`, `$item`).
1134
+ *
1135
+ * This is the counterpart of `isValidJSONPathStrict` for paths whose
1136
+ * root is a bound variable rather than the document. Such a string is
1137
+ * not a valid RFC 9535 query - the RFC's root identifier is `$` alone -
1138
+ * so it can only be checked by recognizing the head and validating the
1139
+ * tail as segments, which is what the query normalizer does before it
1140
+ * raises `JQ0004`. Without this, a schema could only pattern-check the
1141
+ * head and had to leave the segment grammar to the compiler.
1142
+ *
1143
+ * @param {string} str - The variable-rooted path string to validate
1144
+ * @param {JSONPathOptions} [options] - Parse options
1145
+ * @returns {boolean} True when the string is a well-formed variable-rooted path
1146
+ * @example
1147
+ * isValidJSONPathSegments('$book.price'); // true
1148
+ * isValidJSONPathSegments('$book'); // true (no segments)
1149
+ * isValidJSONPathSegments('$book.price['); // false (unterminated segment)
1150
+ * isValidJSONPathSegments('$.price'); // false (no variable name; that is json-path)
1151
+ */
1152
+ export function isValidJSONPathSegments(str, options = undefined) {
1153
+ if (typeof str !== 'string')
1154
+ return false;
1155
+ const head = RE_JSONPATH_VARIABLE_HEAD.exec(str);
1156
+ if (head === null)
1157
+ return false;
1158
+ const rest = str.slice(head[0].length);
1159
+ if (rest === '')
1160
+ return true;
1161
+ try {
1162
+ // the tail is validated by parsing it under a substituted root,
1163
+ // exactly as the normalizer compiles it
1164
+ parseJSONPath('$' + rest, options);
1165
+ return true;
1166
+ }
1167
+ catch (error) {
1168
+ if (!(error instanceof JSONPathSyntaxError))
1169
+ throw error;
1170
+ return false;
1171
+ }
1172
+ }
1173
+
1174
+ //#endregion
1175
+
1176
+ //#region normalized path <-> JSON Pointer bridge
1177
+
1178
+ /**
1179
+ * Convert a singular JSONPath query - which includes every RFC 9535
1180
+ * normalized path - to an RFC 6901 JSON Pointer, so the two addressing
1181
+ * standards compose.
1182
+ *
1183
+ * Any singular form is accepted (`$['store']['book'][0]`, `$.store.book[0]`);
1184
+ * non-singular queries and negative (from-the-end) indexes are rejected,
1185
+ * because a pointer cannot express them.
1186
+ *
1187
+ * @param {string} source - A singular JSONPath query
1188
+ * @returns {string} The equivalent JSON Pointer
1189
+ * @throws {JSONPathSyntaxError} When the query is invalid, not singular,
1190
+ * or uses a negative index
1191
+ * @example
1192
+ * jsonPointerFromJSONPath("$['store']['book'][0]['a/b']"); // '/store/book/0/a~1b'
1193
+ */
1194
+ export function jsonPointerFromJSONPath(source) {
1195
+ const { segments } = parseJSONPath(source);
1196
+ if (!isSingularSegments(segments))
1197
+ throw new JSONPathSyntaxError('only a singular query converts to a JSON Pointer', source, 0);
1198
+ let pointer = '';
1199
+ for (let i = 0; i < segments.length; i++) {
1200
+ const sel = segments[i].selectors[0];
1201
+ if (sel.kind === 'name') {
1202
+ pointer += '/' + encodeJSONPointerSegment(sel.name);
1203
+ }
1204
+ else {
1205
+ if (sel.index < 0)
1206
+ throw new JSONPathSyntaxError('a negative index has no JSON Pointer form', source, 0);
1207
+ pointer += '/' + sel.index;
1208
+ }
1209
+ }
1210
+ return pointer;
1211
+ }
1212
+
1213
+ /**
1214
+ * Convert an RFC 6901 JSON Pointer to an RFC 9535 normalized path.
1215
+ *
1216
+ * A pointer token is one text with two readings (RFC 6901 lets `"2"`
1217
+ * address both a `"2"` member and array element 2); a normalized path
1218
+ * must pick one. Convention: a token that is a valid array index (digits,
1219
+ * no leading zeros) becomes an index selector `[2]`, everything else a
1220
+ * name selector `['name']`. A pointer addressing an object member that
1221
+ * merely looks like an index is therefore converted to the index form -
1222
+ * convert with the document in hand (e.g. via `query.paths`) when that
1223
+ * distinction matters.
1224
+ *
1225
+ * @param {string} pointer - The JSON Pointer (e.g. `/store/book/0`)
1226
+ * @returns {string} The normalized path (e.g. `$['store']['book'][0]`)
1227
+ * @throws {JSONPointerSyntaxError} When the pointer is not valid RFC 6901
1228
+ * @example
1229
+ * jsonPathFromJSONPointer('/store/book/0'); // "$['store']['book'][0]"
1230
+ */
1231
+ export function jsonPathFromJSONPointer(pointer) {
1232
+ const segments = parseJSONPointer(pointer);
1233
+ let path = '$';
1234
+ for (let i = 0; i < segments.length; i++) {
1235
+ const name = segments[i];
1236
+ const index = scanArrayIndex(name, 0, name.length);
1237
+ path = index >= 0 ? path + '[' + index + ']' : appendName(path, name);
1238
+ }
1239
+ return path;
1240
+ }
1241
+
975
1242
  //#endregion
976
1243
 
977
1244
  //#endregion