@swaggerexpert/jsonpath 3.2.4 → 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.
- package/README.md +225 -18
- package/cjs/errors/JSONNormalizedPathError.cjs +8 -0
- package/cjs/errors/JSONPathError.cjs +1 -1
- package/cjs/errors/{JSONPathCompileError.cjs → JSONPathEvaluateError.cjs} +2 -2
- package/cjs/evaluate/evaluators/comparable.cjs +44 -0
- package/cjs/evaluate/evaluators/comparison-expr.cjs +37 -0
- package/cjs/evaluate/evaluators/filter-query.cjs +182 -0
- package/cjs/evaluate/evaluators/function-expr.cjs +106 -0
- package/cjs/evaluate/evaluators/literal.cjs +25 -0
- package/cjs/evaluate/evaluators/logical-expr.cjs +96 -0
- package/cjs/evaluate/evaluators/singular-query.cjs +103 -0
- package/cjs/evaluate/functions/count.cjs +35 -0
- package/cjs/evaluate/functions/index.cjs +15 -0
- package/cjs/evaluate/functions/length.cjs +42 -0
- package/cjs/evaluate/functions/match.cjs +49 -0
- package/cjs/evaluate/functions/search.cjs +49 -0
- package/cjs/evaluate/functions/value.cjs +36 -0
- package/cjs/evaluate/index.cjs +182 -0
- package/cjs/evaluate/realms/EvaluationRealm.cjs +154 -0
- package/cjs/evaluate/realms/json/index.cjs +246 -0
- package/cjs/evaluate/utils/guards.cjs +129 -0
- package/cjs/evaluate/utils/i-regexp.cjs +118 -0
- package/cjs/evaluate/visitors/bracketed-selection.cjs +35 -0
- package/cjs/evaluate/visitors/filter-selector.cjs +43 -0
- package/cjs/evaluate/visitors/index-selector.cjs +55 -0
- package/cjs/evaluate/visitors/name-selector.cjs +38 -0
- package/cjs/evaluate/visitors/segment.cjs +99 -0
- package/cjs/evaluate/visitors/selector.cjs +47 -0
- package/cjs/evaluate/visitors/slice-selector.cjs +115 -0
- package/cjs/evaluate/visitors/wildcard-selector.cjs +32 -0
- package/cjs/index.cjs +16 -7
- package/cjs/normalized-path.cjs +145 -0
- package/cjs/parse/callbacks/cst.cjs +2 -4
- package/cjs/parse/index.cjs +3 -1
- package/cjs/parse/translators/ASTTranslator/index.cjs +1 -1
- package/cjs/parse/translators/ASTTranslator/transformers.cjs +246 -5
- package/cjs/parse/translators/CSTOptimizedTranslator.cjs +1 -3
- package/cjs/parse/translators/CSTTranslator.cjs +1 -2
- package/cjs/test/index.cjs +4 -2
- package/es/errors/JSONNormalizedPathError.mjs +3 -0
- package/es/errors/JSONPathError.mjs +1 -1
- package/es/errors/JSONPathEvaluateError.mjs +3 -0
- package/es/evaluate/evaluators/comparable.mjs +38 -0
- package/es/evaluate/evaluators/comparison-expr.mjs +31 -0
- package/es/evaluate/evaluators/filter-query.mjs +175 -0
- package/es/evaluate/evaluators/function-expr.mjs +99 -0
- package/es/evaluate/evaluators/literal.mjs +21 -0
- package/es/evaluate/evaluators/logical-expr.mjs +89 -0
- package/es/evaluate/evaluators/singular-query.mjs +97 -0
- package/es/evaluate/functions/count.mjs +30 -0
- package/es/evaluate/functions/index.mjs +13 -0
- package/es/evaluate/functions/length.mjs +37 -0
- package/es/evaluate/functions/match.mjs +44 -0
- package/es/evaluate/functions/search.mjs +44 -0
- package/es/evaluate/functions/value.mjs +31 -0
- package/es/evaluate/index.mjs +174 -0
- package/es/evaluate/realms/EvaluationRealm.mjs +148 -0
- package/es/evaluate/realms/json/index.mjs +240 -0
- package/es/evaluate/utils/guards.mjs +114 -0
- package/es/evaluate/utils/i-regexp.mjs +113 -0
- package/es/evaluate/visitors/bracketed-selection.mjs +29 -0
- package/es/evaluate/visitors/filter-selector.mjs +37 -0
- package/es/evaluate/visitors/index-selector.mjs +51 -0
- package/es/evaluate/visitors/name-selector.mjs +34 -0
- package/es/evaluate/visitors/segment.mjs +91 -0
- package/es/evaluate/visitors/selector.mjs +41 -0
- package/es/evaluate/visitors/slice-selector.mjs +111 -0
- package/es/evaluate/visitors/wildcard-selector.mjs +28 -0
- package/es/index.mjs +7 -3
- package/es/normalized-path.mjs +136 -0
- package/es/parse/callbacks/cst.mjs +2 -4
- package/es/parse/index.mjs +3 -1
- package/es/parse/translators/ASTTranslator/index.mjs +1 -1
- package/es/parse/translators/ASTTranslator/transformers.mjs +246 -5
- package/es/parse/translators/CSTOptimizedTranslator.mjs +1 -3
- package/es/parse/translators/CSTTranslator.mjs +1 -2
- package/es/test/index.mjs +4 -2
- package/package.json +4 -2
- package/types/index.d.ts +135 -8
- package/cjs/compile.cjs +0 -50
- package/cjs/escape.cjs +0 -59
- package/es/compile.mjs +0 -45
- package/es/errors/JSONPathCompileError.mjs +0 -3
- 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
|
+
};
|