@swaggerexpert/jsonpath 3.2.5 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +225 -18
  2. package/cjs/errors/JSONNormalizedPathError.cjs +8 -0
  3. package/cjs/errors/JSONPathError.cjs +1 -1
  4. package/cjs/errors/{JSONPathCompileError.cjs → JSONPathEvaluateError.cjs} +2 -2
  5. package/cjs/evaluate/evaluators/comparable.cjs +44 -0
  6. package/cjs/evaluate/evaluators/comparison-expr.cjs +37 -0
  7. package/cjs/evaluate/evaluators/filter-query.cjs +182 -0
  8. package/cjs/evaluate/evaluators/function-expr.cjs +106 -0
  9. package/cjs/evaluate/evaluators/literal.cjs +25 -0
  10. package/cjs/evaluate/evaluators/logical-expr.cjs +96 -0
  11. package/cjs/evaluate/evaluators/singular-query.cjs +103 -0
  12. package/cjs/evaluate/functions/count.cjs +35 -0
  13. package/cjs/evaluate/functions/index.cjs +15 -0
  14. package/cjs/evaluate/functions/length.cjs +42 -0
  15. package/cjs/evaluate/functions/match.cjs +49 -0
  16. package/cjs/evaluate/functions/search.cjs +49 -0
  17. package/cjs/evaluate/functions/value.cjs +36 -0
  18. package/cjs/evaluate/index.cjs +182 -0
  19. package/cjs/evaluate/realms/EvaluationRealm.cjs +154 -0
  20. package/cjs/evaluate/realms/json/index.cjs +246 -0
  21. package/cjs/evaluate/utils/guards.cjs +129 -0
  22. package/cjs/evaluate/utils/i-regexp.cjs +118 -0
  23. package/cjs/evaluate/visitors/bracketed-selection.cjs +35 -0
  24. package/cjs/evaluate/visitors/filter-selector.cjs +43 -0
  25. package/cjs/evaluate/visitors/index-selector.cjs +55 -0
  26. package/cjs/evaluate/visitors/name-selector.cjs +38 -0
  27. package/cjs/evaluate/visitors/segment.cjs +99 -0
  28. package/cjs/evaluate/visitors/selector.cjs +47 -0
  29. package/cjs/evaluate/visitors/slice-selector.cjs +115 -0
  30. package/cjs/evaluate/visitors/wildcard-selector.cjs +32 -0
  31. package/cjs/index.cjs +16 -7
  32. package/cjs/normalized-path.cjs +145 -0
  33. package/cjs/parse/callbacks/cst.cjs +2 -4
  34. package/cjs/parse/index.cjs +3 -1
  35. package/cjs/parse/translators/ASTTranslator/index.cjs +1 -1
  36. package/cjs/parse/translators/ASTTranslator/transformers.cjs +246 -5
  37. package/cjs/parse/translators/CSTOptimizedTranslator.cjs +1 -3
  38. package/cjs/parse/translators/CSTTranslator.cjs +1 -2
  39. package/cjs/test/index.cjs +4 -2
  40. package/es/errors/JSONNormalizedPathError.mjs +3 -0
  41. package/es/errors/JSONPathError.mjs +1 -1
  42. package/es/errors/JSONPathEvaluateError.mjs +3 -0
  43. package/es/evaluate/evaluators/comparable.mjs +38 -0
  44. package/es/evaluate/evaluators/comparison-expr.mjs +31 -0
  45. package/es/evaluate/evaluators/filter-query.mjs +175 -0
  46. package/es/evaluate/evaluators/function-expr.mjs +99 -0
  47. package/es/evaluate/evaluators/literal.mjs +21 -0
  48. package/es/evaluate/evaluators/logical-expr.mjs +89 -0
  49. package/es/evaluate/evaluators/singular-query.mjs +97 -0
  50. package/es/evaluate/functions/count.mjs +30 -0
  51. package/es/evaluate/functions/index.mjs +13 -0
  52. package/es/evaluate/functions/length.mjs +37 -0
  53. package/es/evaluate/functions/match.mjs +44 -0
  54. package/es/evaluate/functions/search.mjs +44 -0
  55. package/es/evaluate/functions/value.mjs +31 -0
  56. package/es/evaluate/index.mjs +174 -0
  57. package/es/evaluate/realms/EvaluationRealm.mjs +148 -0
  58. package/es/evaluate/realms/json/index.mjs +240 -0
  59. package/es/evaluate/utils/guards.mjs +114 -0
  60. package/es/evaluate/utils/i-regexp.mjs +113 -0
  61. package/es/evaluate/visitors/bracketed-selection.mjs +29 -0
  62. package/es/evaluate/visitors/filter-selector.mjs +37 -0
  63. package/es/evaluate/visitors/index-selector.mjs +51 -0
  64. package/es/evaluate/visitors/name-selector.mjs +34 -0
  65. package/es/evaluate/visitors/segment.mjs +91 -0
  66. package/es/evaluate/visitors/selector.mjs +41 -0
  67. package/es/evaluate/visitors/slice-selector.mjs +111 -0
  68. package/es/evaluate/visitors/wildcard-selector.mjs +28 -0
  69. package/es/index.mjs +7 -3
  70. package/es/normalized-path.mjs +136 -0
  71. package/es/parse/callbacks/cst.mjs +2 -4
  72. package/es/parse/index.mjs +3 -1
  73. package/es/parse/translators/ASTTranslator/index.mjs +1 -1
  74. package/es/parse/translators/ASTTranslator/transformers.mjs +246 -5
  75. package/es/parse/translators/CSTOptimizedTranslator.mjs +1 -3
  76. package/es/parse/translators/CSTTranslator.mjs +1 -2
  77. package/es/test/index.mjs +4 -2
  78. package/package.json +4 -2
  79. package/types/index.d.ts +114 -11
  80. package/cjs/compile.cjs +0 -50
  81. package/cjs/escape.cjs +0 -59
  82. package/es/compile.mjs +0 -45
  83. package/es/errors/JSONPathCompileError.mjs +0 -3
  84. package/es/escape.mjs +0 -55
@@ -0,0 +1,174 @@
1
+ /**
2
+ * JSONPath evaluation module.
3
+ *
4
+ * Provides the evaluate() function to execute JSONPath expressions against a value.
5
+ * Uses an explicit stack for tree traversal to avoid call stack overflow
6
+ * on deeply nested documents.
7
+ *
8
+ * @module evaluate
9
+ * @see https://www.rfc-editor.org/rfc/rfc9535
10
+ */
11
+ import parse from "../parse/index.mjs";
12
+ import * as NormalizedPath from "../normalized-path.mjs";
13
+ import visitSegment from "./visitors/segment.mjs";
14
+ import JSONEvaluationRealm from "./realms/json/index.mjs";
15
+ import JSONPathEvaluateError from "../errors/JSONPathEvaluateError.mjs";
16
+ import * as defaultFunctions from "./functions/index.mjs";
17
+ /**
18
+ * @typedef {Object} EvaluateOptions
19
+ * @property {Function} [callback] - Optional callback (value, normalizedPath) => void
20
+ * Called for each match. Allows streaming results and collecting paths.
21
+ * @property {Object} [realm] - Optional custom evaluation realm.
22
+ * Default is JSONEvaluationRealm for plain objects/arrays.
23
+ * @property {Object} [functions] - Optional custom function registry.
24
+ * Can extend or override built-in functions (length, count, match, search, value).
25
+ */
26
+ /**
27
+ * Evaluate a JSONPath expression against a value.
28
+ *
29
+ * @param {unknown} value - JSON value to query
30
+ * @param {string} expression - JSONPath expression
31
+ * @param {EvaluateOptions} [options] - Evaluation options
32
+ * @returns {unknown[]} - Array of matched values
33
+ * @throws {JSONPathEvaluateError} If the expression is invalid
34
+ *
35
+ * @example
36
+ * // Simple query
37
+ * evaluate({ a: 1, b: 2 }, '$.a');
38
+ * // => [1]
39
+ *
40
+ * @example
41
+ * // Wildcard
42
+ * evaluate({ store: { book: [{ title: 'A' }, { title: 'B' }] } }, '$.store.book[*].title');
43
+ * // => ['A', 'B']
44
+ *
45
+ * @example
46
+ * // With callback to collect paths
47
+ * const paths = [];
48
+ * evaluate(value, '$.store.book[*]', {
49
+ * callback: (v, path) => paths.push(path)
50
+ * });
51
+ */
52
+ const evaluate = (value, expression, {
53
+ callback,
54
+ realm = new JSONEvaluationRealm(),
55
+ functions = defaultFunctions
56
+ } = {}) => {
57
+ // Parse the expression
58
+ const parseResult = parse(expression);
59
+ if (!parseResult.result.success) {
60
+ throw new JSONPathEvaluateError(`Invalid JSONPath expression: ${expression}`, {
61
+ expression
62
+ });
63
+ }
64
+ try {
65
+ // The tree is the AST root directly (JsonPathQuery node)
66
+ const ast = parseResult.tree;
67
+ const {
68
+ segments
69
+ } = ast;
70
+ const results = [];
71
+
72
+ // Handle empty query ($ with no segments)
73
+ if (segments.length === 0) {
74
+ results.push(value);
75
+ if (typeof callback === 'function') {
76
+ callback(value, '$');
77
+ }
78
+ return results;
79
+ }
80
+
81
+ // Evaluation context with root for filter expressions
82
+ const ctx = {
83
+ realm,
84
+ root: value,
85
+ functions
86
+ };
87
+ const stack = [];
88
+
89
+ // Start with root value
90
+ stack.push({
91
+ value,
92
+ path: [],
93
+ segmentIndex: 0
94
+ });
95
+ while (stack.length > 0) {
96
+ const item = stack.pop();
97
+ const {
98
+ value,
99
+ path,
100
+ segmentIndex
101
+ } = item;
102
+
103
+ // If all segments processed, emit result
104
+ if (segmentIndex >= segments.length) {
105
+ const normalizedPath = NormalizedPath.from(path);
106
+ results.push(value);
107
+ if (typeof callback === 'function') {
108
+ callback(value, normalizedPath);
109
+ }
110
+ continue;
111
+ }
112
+ const segment = segments[segmentIndex];
113
+
114
+ // Collect results from this segment
115
+ const segmentResults = [];
116
+ const emit = (selectedValue, pathSegment) => {
117
+ segmentResults.push({
118
+ value: selectedValue,
119
+ pathSegment
120
+ });
121
+ };
122
+
123
+ // Apply segment
124
+ visitSegment(ctx, value, segment, emit);
125
+
126
+ // For descendant segments, also push children for recursive descent
127
+ // Push descendants FIRST so they're processed AFTER current level results (LIFO)
128
+ if (segment.type === 'DescendantSegment') {
129
+ const descendants = [];
130
+ for (const [key, child] of realm.entries(value)) {
131
+ descendants.push({
132
+ value: child,
133
+ pathSegment: key
134
+ });
135
+ }
136
+
137
+ // Push descendants (in reverse for correct order)
138
+ // They stay at same segment index to continue recursive descent
139
+ for (let i = descendants.length - 1; i >= 0; i -= 1) {
140
+ const {
141
+ value: descendantValue,
142
+ pathSegment
143
+ } = descendants[i];
144
+ stack.push({
145
+ value: descendantValue,
146
+ path: [...path, pathSegment],
147
+ segmentIndex // Same segment for recursive descent
148
+ });
149
+ }
150
+ }
151
+
152
+ // Push results for next segment (in reverse order for correct output order)
153
+ // Push these AFTER descendants so they're processed FIRST (LIFO = document order)
154
+ for (let i = segmentResults.length - 1; i >= 0; i -= 1) {
155
+ const {
156
+ value: selectedValue,
157
+ pathSegment
158
+ } = segmentResults[i];
159
+ stack.push({
160
+ value: selectedValue,
161
+ path: [...path, pathSegment],
162
+ segmentIndex: segmentIndex + 1
163
+ });
164
+ }
165
+ }
166
+ return results;
167
+ } catch (error) {
168
+ throw new JSONPathEvaluateError('Unexpected error during JSONPath evaluation', {
169
+ cause: error,
170
+ expression
171
+ });
172
+ }
173
+ };
174
+ export default evaluate;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Abstract base class for Evaluation Realms.
3
+ *
4
+ * Evaluation Realms provide an abstraction for accessing different data structures,
5
+ * allowing JSONPath evaluation to work with various data types.
6
+ *
7
+ * Subclasses must implement all abstract methods.
8
+ */
9
+ import JSONPathError from "../../errors/JSONPathError.mjs";
10
+ class EvaluationRealm {
11
+ name = '';
12
+
13
+ /**
14
+ * Check if value is object (has named properties).
15
+ * @param {unknown} value
16
+ * @returns {boolean}
17
+ */
18
+ isObject(value) {
19
+ throw new JSONPathError('Realm.isObject(value) must be implemented in a subclass');
20
+ }
21
+
22
+ /**
23
+ * Check if value is array (has indexed elements).
24
+ * @param {unknown} value
25
+ * @returns {boolean}
26
+ */
27
+ isArray(value) {
28
+ throw new JSONPathError('Realm.isArray(value) must be implemented in a subclass');
29
+ }
30
+
31
+ /**
32
+ * Check if value is string.
33
+ * @param {unknown} value
34
+ * @returns {boolean}
35
+ */
36
+ isString(value) {
37
+ throw new JSONPathError('Realm.isString(value) must be implemented in a subclass');
38
+ }
39
+
40
+ /**
41
+ * Check if value is number.
42
+ * @param {unknown} value
43
+ * @returns {boolean}
44
+ */
45
+ isNumber(value) {
46
+ throw new JSONPathError('Realm.isNumber(value) must be implemented in a subclass');
47
+ }
48
+
49
+ /**
50
+ * Check if value is boolean.
51
+ * @param {unknown} value
52
+ * @returns {boolean}
53
+ */
54
+ isBoolean(value) {
55
+ throw new JSONPathError('Realm.isBoolean(value) must be implemented in a subclass');
56
+ }
57
+
58
+ /**
59
+ * Check if value is null.
60
+ * @param {unknown} value
61
+ * @returns {boolean}
62
+ */
63
+ isNull(value) {
64
+ throw new JSONPathError('Realm.isNull(value) must be implemented in a subclass');
65
+ }
66
+
67
+ /**
68
+ * Get raw string value for regex operations.
69
+ * @param {unknown} value
70
+ * @returns {string | undefined}
71
+ */
72
+ getString(value) {
73
+ throw new JSONPathError('Realm.getString(value) must be implemented in a subclass');
74
+ }
75
+
76
+ /**
77
+ * Get property by name from object value.
78
+ * @param {unknown} value
79
+ * @param {string} key
80
+ * @returns {unknown}
81
+ */
82
+ getProperty(value, key) {
83
+ throw new JSONPathError('Realm.getProperty(value, key) must be implemented in a subclass');
84
+ }
85
+
86
+ /**
87
+ * Check if object value has property.
88
+ * @param {unknown} value
89
+ * @param {string} key
90
+ * @returns {boolean}
91
+ */
92
+ hasProperty(value, key) {
93
+ throw new JSONPathError('Realm.hasProperty(value, key) must be implemented in a subclass');
94
+ }
95
+
96
+ /**
97
+ * Get element by index from array value.
98
+ * @param {unknown} value
99
+ * @param {number} index
100
+ * @returns {unknown}
101
+ */
102
+ getElement(value, index) {
103
+ throw new JSONPathError('Realm.getElement(value, index) must be implemented in a subclass');
104
+ }
105
+
106
+ /**
107
+ * Get all keys of object value.
108
+ * @param {unknown} value
109
+ * @returns {string[]}
110
+ */
111
+ getKeys(value) {
112
+ throw new JSONPathError('Realm.getKeys(value) must be implemented in a subclass');
113
+ }
114
+
115
+ /**
116
+ * Get length of array or object value.
117
+ * @param {unknown} value
118
+ * @returns {number}
119
+ */
120
+ getLength(value) {
121
+ throw new JSONPathError('Realm.getLength(value) must be implemented in a subclass');
122
+ }
123
+
124
+ /**
125
+ * Iterate over entries as [key/index, value] pairs.
126
+ * For objects: yields [key, value] for each member.
127
+ * For arrays: yields [index, value] for each element.
128
+ * For other types: yields nothing.
129
+ * @param {unknown} value
130
+ * @returns {Iterable<[string | number, unknown]>}
131
+ */
132
+ *entries(value) {
133
+ throw new JSONPathError('Realm.entries(value) must be implemented in a subclass');
134
+ }
135
+
136
+ /**
137
+ * Compare two values using the specified operator.
138
+ * Per RFC 9535 Section 2.3.5.2.3.
139
+ * @param {unknown} left
140
+ * @param {string} operator - One of: ==, !=, <, <=, >, >=
141
+ * @param {unknown} right
142
+ * @returns {boolean}
143
+ */
144
+ compare(left, operator, right) {
145
+ throw new JSONPathError('Realm.compare(left, operator, right) must be implemented in a subclass');
146
+ }
147
+ }
148
+ export default EvaluationRealm;
@@ -0,0 +1,240 @@
1
+ /**
2
+ * JSON Evaluation Realm for plain JavaScript objects and arrays.
3
+ */
4
+ import EvaluationRealm from "../EvaluationRealm.mjs";
5
+ import { isPlainObject, isArray, isNothing, isString, isNumber, isBoolean, isNull } from "../../utils/guards.mjs";
6
+ /**
7
+ * JSON Evaluation Realm implementation.
8
+ */
9
+ class JSONEvaluationRealm extends EvaluationRealm {
10
+ name = 'json';
11
+
12
+ /**
13
+ * Check if value is object (plain object with named properties).
14
+ * @param {unknown} value
15
+ * @returns {boolean}
16
+ */
17
+ isObject(value) {
18
+ return isPlainObject(value);
19
+ }
20
+
21
+ /**
22
+ * Check if value is array.
23
+ * @param {unknown} value
24
+ * @returns {boolean}
25
+ */
26
+ isArray(value) {
27
+ return isArray(value);
28
+ }
29
+
30
+ /**
31
+ * Check if value is string.
32
+ * @param {unknown} value
33
+ * @returns {boolean}
34
+ */
35
+ isString(value) {
36
+ return isString(value);
37
+ }
38
+
39
+ /**
40
+ * Check if value is number.
41
+ * @param {unknown} value
42
+ * @returns {boolean}
43
+ */
44
+ isNumber(value) {
45
+ return isNumber(value);
46
+ }
47
+
48
+ /**
49
+ * Check if value is boolean.
50
+ * @param {unknown} value
51
+ * @returns {boolean}
52
+ */
53
+ isBoolean(value) {
54
+ return isBoolean(value);
55
+ }
56
+
57
+ /**
58
+ * Check if value is null.
59
+ * @param {unknown} value
60
+ * @returns {boolean}
61
+ */
62
+ isNull(value) {
63
+ return isNull(value);
64
+ }
65
+
66
+ /**
67
+ * Get raw string value for regex operations.
68
+ * @param {unknown} value
69
+ * @returns {string | undefined}
70
+ */
71
+ getString(value) {
72
+ return isString(value) ? value : undefined;
73
+ }
74
+
75
+ /**
76
+ * Get property by name from object value.
77
+ * Returns undefined if property doesn't exist (Nothing).
78
+ * @param {unknown} value
79
+ * @param {string} key
80
+ * @returns {unknown}
81
+ */
82
+ getProperty(value, key) {
83
+ if (!isPlainObject(value)) return undefined;
84
+ return Object.hasOwn(value, key) ? value[key] : undefined;
85
+ }
86
+
87
+ /**
88
+ * Check if object value has property.
89
+ * @param {unknown} value
90
+ * @param {string} key
91
+ * @returns {boolean}
92
+ */
93
+ hasProperty(value, key) {
94
+ if (!isPlainObject(value)) return false;
95
+ return Object.hasOwn(value, key);
96
+ }
97
+
98
+ /**
99
+ * Get element by index from array value.
100
+ * Returns undefined if index out of bounds (Nothing).
101
+ * @param {unknown} value
102
+ * @param {number} index
103
+ * @returns {unknown}
104
+ */
105
+ getElement(value, index) {
106
+ if (!isArray(value)) return undefined;
107
+ if (index < 0 || index >= value.length) return undefined;
108
+ return value[index];
109
+ }
110
+
111
+ /**
112
+ * Get all keys of object value.
113
+ * @param {unknown} value
114
+ * @returns {string[]}
115
+ */
116
+ getKeys(value) {
117
+ if (!isPlainObject(value)) return [];
118
+ return Object.keys(value);
119
+ }
120
+
121
+ /**
122
+ * Get length of value.
123
+ * Per RFC 9535 Section 2.4.5:
124
+ * - String: number of Unicode scalar values (not UTF-16 code units)
125
+ * - Array: number of elements
126
+ * - Object: number of members
127
+ * @param {unknown} value
128
+ * @returns {number}
129
+ */
130
+ getLength(value) {
131
+ if (isString(value)) return [...value].length;
132
+ if (isArray(value)) return value.length;
133
+ if (isPlainObject(value)) return Object.keys(value).length;
134
+ return 0;
135
+ }
136
+
137
+ /**
138
+ * Iterate over entries as [key/index, value] pairs.
139
+ * For objects: yields [key, value] for each member.
140
+ * For arrays: yields [index, value] for each element.
141
+ * For other types: yields nothing.
142
+ * @param {unknown} value
143
+ * @returns {Iterable<[string | number, unknown]>}
144
+ */
145
+ *entries(value) {
146
+ if (this.isArray(value)) {
147
+ for (let i = 0; i < value.length; i += 1) {
148
+ yield [i, value[i]];
149
+ }
150
+ } else if (this.isObject(value)) {
151
+ for (const key of Object.keys(value)) {
152
+ yield [key, value[key]];
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Deep equality check for JSON values.
159
+ * @param {unknown} a
160
+ * @param {unknown} b
161
+ * @returns {boolean}
162
+ */
163
+ #deepEqual(a, b) {
164
+ // Primitive comparison
165
+ if (a === b) return true;
166
+
167
+ // Nothing comparison
168
+ if (isNothing(a) && isNothing(b)) return true;
169
+ if (isNothing(a) || isNothing(b)) return false;
170
+
171
+ // Null comparison
172
+ if (isNull(a) && isNull(b)) return true;
173
+ if (isNull(a) || isNull(b)) return false;
174
+
175
+ // Type must match for complex types
176
+ if (typeof a !== typeof b) return false;
177
+
178
+ // Array comparison
179
+ if (isArray(a) && isArray(b)) {
180
+ if (a.length !== b.length) return false;
181
+ for (let i = 0; i < a.length; i += 1) {
182
+ if (!this.#deepEqual(a[i], b[i])) return false;
183
+ }
184
+ return true;
185
+ }
186
+
187
+ // Object comparison
188
+ if (isPlainObject(a) && isPlainObject(b)) {
189
+ const keysA = Object.keys(a);
190
+ const keysB = Object.keys(b);
191
+ if (keysA.length !== keysB.length) return false;
192
+ for (const key of keysA) {
193
+ if (!Object.hasOwn(b, key)) return false;
194
+ if (!this.#deepEqual(a[key], b[key])) return false;
195
+ }
196
+ return true;
197
+ }
198
+ return false;
199
+ }
200
+
201
+ /**
202
+ * Compare two values using the specified operator.
203
+ * Per RFC 9535 Section 2.3.5.2.3.
204
+ * @param {unknown} left
205
+ * @param {string} operator - One of: ==, !=, <, <=, >, >=
206
+ * @param {unknown} right
207
+ * @returns {boolean}
208
+ */
209
+ compare(left, operator, right) {
210
+ switch (operator) {
211
+ case '==':
212
+ return this.#deepEqual(left, right);
213
+ case '!=':
214
+ return !this.#deepEqual(left, right);
215
+ case '<':
216
+ if (isNothing(left) || isNothing(right)) return false;
217
+ if (isNumber(left) && isNumber(right)) return left < right;
218
+ if (isString(left) && isString(right)) return left < right;
219
+ return false;
220
+ case '<=':
221
+ if (isNothing(left) || isNothing(right)) return false;
222
+ if (isNumber(left) && isNumber(right)) return left <= right;
223
+ if (isString(left) && isString(right)) return left <= right;
224
+ return this.#deepEqual(left, right);
225
+ case '>':
226
+ if (isNothing(left) || isNothing(right)) return false;
227
+ if (isNumber(left) && isNumber(right)) return left > right;
228
+ if (isString(left) && isString(right)) return left > right;
229
+ return false;
230
+ case '>=':
231
+ if (isNothing(left) || isNothing(right)) return false;
232
+ if (isNumber(left) && isNumber(right)) return left >= right;
233
+ if (isString(left) && isString(right)) return left >= right;
234
+ return this.#deepEqual(left, right);
235
+ default:
236
+ return false;
237
+ }
238
+ }
239
+ }
240
+ export default JSONEvaluationRealm;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Type guards for JSON value types.
3
+ */
4
+
5
+ const objectTag = '[object Object]';
6
+
7
+ /**
8
+ * Check if value is an array.
9
+ * @param {unknown} value
10
+ * @returns {value is unknown[]}
11
+ */
12
+ export const isArray = value => Array.isArray(value);
13
+
14
+ /**
15
+ * Check if value is an object (not null, not array).
16
+ * @param {unknown} value
17
+ * @returns {value is object}
18
+ */
19
+ export const isObject = value => typeof value === 'object' && value !== null && !isArray(value);
20
+
21
+ /**
22
+ * Check if value is a plain object (created by Object constructor or object literal).
23
+ * @param {unknown} value
24
+ * @returns {value is Record<string, unknown>}
25
+ */
26
+ export const isPlainObject = value => {
27
+ if (!isObject(value)) return false;
28
+ const proto = Object.getPrototypeOf(value);
29
+ if (proto === null) return true;
30
+ return proto.constructor === Object && Object.prototype.toString.call(value) === objectTag;
31
+ };
32
+
33
+ /**
34
+ * Check if value is a string.
35
+ * @param {unknown} value
36
+ * @returns {value is string}
37
+ */
38
+ export const isString = value => typeof value === 'string';
39
+
40
+ /**
41
+ * Check if value is a number.
42
+ * @param {unknown} value
43
+ * @returns {value is number}
44
+ */
45
+ export const isNumber = value => typeof value === 'number' && Number.isFinite(value);
46
+
47
+ /**
48
+ * Check if value is a boolean.
49
+ * @param {unknown} value
50
+ * @returns {value is boolean}
51
+ */
52
+ export const isBoolean = value => typeof value === 'boolean';
53
+
54
+ /**
55
+ * Check if value is null.
56
+ * @param {unknown} value
57
+ * @returns {value is null}
58
+ */
59
+ export const isNull = value => value === null;
60
+
61
+ /**
62
+ * Check if value represents Nothing (undefined).
63
+ * Per RFC 9535, Nothing is used when a query returns no value.
64
+ * We use undefined since JSON has no undefined value.
65
+ * @param {unknown} value
66
+ * @returns {value is undefined}
67
+ */
68
+ export const isNothing = value => value === undefined;
69
+
70
+ /**
71
+ * Check if value is a valid JSON value.
72
+ * @param {unknown} value
73
+ * @returns {boolean}
74
+ */
75
+ export const isJsonValue = value => {
76
+ if (isNull(value) || isBoolean(value) || isString(value) || isNumber(value)) {
77
+ return true;
78
+ }
79
+ if (isArray(value)) {
80
+ return value.every(isJsonValue);
81
+ }
82
+ if (isPlainObject(value)) {
83
+ return Object.values(value).every(isJsonValue);
84
+ }
85
+ return false;
86
+ };
87
+
88
+ /**
89
+ * Check if value is a nodelist (marked array from filter query).
90
+ * @param {unknown} value
91
+ * @returns {boolean}
92
+ */
93
+ export const isNodelist = value => isArray(value) && value._isNodelist === true;
94
+
95
+ /**
96
+ * Coerce a nodelist to a single value (ValueType).
97
+ * Per RFC 9535 Section 2.4.1: if function expects ValueType and receives NodesType,
98
+ * auto-convert: single node -> that node's value, otherwise -> Nothing.
99
+ *
100
+ * @param {unknown} value - Input value (may be a nodelist)
101
+ * @returns {unknown} - Coerced value or Nothing (undefined)
102
+ */
103
+ export const coerceToValueType = value => {
104
+ if (isNodelist(value)) {
105
+ // Single node: unwrap and return value
106
+ if (value.length === 1) {
107
+ return value[0];
108
+ }
109
+ // Empty or multiple nodes: return Nothing
110
+ return undefined;
111
+ }
112
+ // Not a nodelist, return as-is
113
+ return value;
114
+ };