@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.
- 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 +114 -11
- 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,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* I-Regexp (RFC 9485) utilities for JSONPath match() and search() functions.
|
|
3
|
+
*
|
|
4
|
+
* @see https://www.rfc-editor.org/rfc/rfc9485 - I-Regexp specification
|
|
5
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535#section-2.4.1 - match() function
|
|
6
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535#section-2.4.2 - search() function
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const regexpCache = new Map();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate and transform I-Regexp pattern to ECMAScript regex pattern.
|
|
13
|
+
* Returns null if pattern contains non-I-Regexp features:
|
|
14
|
+
* - Backreferences (\1, \2, etc.)
|
|
15
|
+
* - Lookahead/lookbehind assertions
|
|
16
|
+
* - Named capture groups
|
|
17
|
+
* - Word boundaries outside character classes
|
|
18
|
+
*
|
|
19
|
+
* Transforms `.` to `[^\n\r]` outside character classes (I-Regexp semantics).
|
|
20
|
+
*
|
|
21
|
+
* @param {string} pattern
|
|
22
|
+
* @returns {string | null} - Transformed pattern or null if invalid
|
|
23
|
+
*/
|
|
24
|
+
const transformIRegexp = pattern => {
|
|
25
|
+
let result = '';
|
|
26
|
+
let inCharClass = false;
|
|
27
|
+
let i = 0;
|
|
28
|
+
while (i < pattern.length) {
|
|
29
|
+
const ch = pattern[i];
|
|
30
|
+
|
|
31
|
+
// Handle escape sequences
|
|
32
|
+
if (ch === '\\' && i + 1 < pattern.length) {
|
|
33
|
+
const next = pattern[i + 1];
|
|
34
|
+
|
|
35
|
+
// Reject backreferences (\1-\9)
|
|
36
|
+
if (next >= '1' && next <= '9') return null;
|
|
37
|
+
|
|
38
|
+
// Reject word boundaries outside character classes
|
|
39
|
+
if (!inCharClass && (next === 'b' || next === 'B')) return null;
|
|
40
|
+
result += ch + next;
|
|
41
|
+
i += 2;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Track character class boundaries
|
|
46
|
+
if (ch === '[' && !inCharClass) {
|
|
47
|
+
inCharClass = true;
|
|
48
|
+
result += ch;
|
|
49
|
+
i += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (ch === ']' && inCharClass) {
|
|
53
|
+
inCharClass = false;
|
|
54
|
+
result += ch;
|
|
55
|
+
i += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check for lookahead/lookbehind/named groups: (?=, (?!, (?<=, (?<!, (?<name>
|
|
60
|
+
if (ch === '(' && i + 2 < pattern.length && pattern[i + 1] === '?') {
|
|
61
|
+
const next2 = pattern[i + 2];
|
|
62
|
+
// Reject lookahead (?= (?!
|
|
63
|
+
if (next2 === '=' || next2 === '!') return null;
|
|
64
|
+
// Check for lookbehind or named groups
|
|
65
|
+
if (next2 === '<' && i + 3 < pattern.length) {
|
|
66
|
+
const next3 = pattern[i + 3];
|
|
67
|
+
// Reject lookbehind (?<= (?<!
|
|
68
|
+
if (next3 === '=' || next3 === '!') return null;
|
|
69
|
+
// Reject named capture groups (?<name>
|
|
70
|
+
if (/[a-zA-Z]/.test(next3)) return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Transform `.` to `[^\n\r]` outside character classes
|
|
75
|
+
if (ch === '.' && !inCharClass) {
|
|
76
|
+
result += '[^\\n\\r]';
|
|
77
|
+
i += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
result += ch;
|
|
81
|
+
i += 1;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Construct a RegExp from I-Regexp pattern.
|
|
88
|
+
* Validates the pattern, transforms it, and caches the result.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} pattern - I-Regexp pattern
|
|
91
|
+
* @param {boolean} [anchor=false] - If true, anchor pattern with ^(?:...)$
|
|
92
|
+
* @returns {RegExp | null} - Compiled regex or null if invalid
|
|
93
|
+
*/
|
|
94
|
+
export const constructRegex = (pattern, anchor = false) => {
|
|
95
|
+
const cacheKey = anchor ? `anchored:${pattern}` : `unanchored:${pattern}`;
|
|
96
|
+
if (regexpCache.has(cacheKey)) {
|
|
97
|
+
return regexpCache.get(cacheKey);
|
|
98
|
+
}
|
|
99
|
+
const transformed = transformIRegexp(pattern);
|
|
100
|
+
if (transformed === null) {
|
|
101
|
+
regexpCache.set(cacheKey, null);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const finalPattern = anchor ? `^(?:${transformed})$` : transformed;
|
|
106
|
+
const regex = new RegExp(finalPattern, 'u');
|
|
107
|
+
regexpCache.set(cacheKey, regex);
|
|
108
|
+
return regex;
|
|
109
|
+
} catch {
|
|
110
|
+
regexpCache.set(cacheKey, null);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bracketed selection visitor.
|
|
3
|
+
*
|
|
4
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535#section-2.5.1
|
|
5
|
+
*
|
|
6
|
+
* A bracketed selection contains one or more selectors.
|
|
7
|
+
* Each selector's results are concatenated.
|
|
8
|
+
*/
|
|
9
|
+
import visitSelector from "./selector.mjs";
|
|
10
|
+
/**
|
|
11
|
+
* Visit a bracketed selection.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} ctx - Evaluation context
|
|
14
|
+
* @param {unknown} value - Current value
|
|
15
|
+
* @param {object} node - AST node
|
|
16
|
+
* @param {object[]} node.selectors - Array of selector AST nodes
|
|
17
|
+
* @param {(value: unknown, segment: string | number) => void} emit - Callback to emit selected value
|
|
18
|
+
*/
|
|
19
|
+
const visitBracketedSelection = (ctx, value, node, emit) => {
|
|
20
|
+
const {
|
|
21
|
+
selectors
|
|
22
|
+
} = node;
|
|
23
|
+
|
|
24
|
+
// Visit each selector and emit its results
|
|
25
|
+
for (const selector of selectors) {
|
|
26
|
+
visitSelector(ctx, value, selector, emit);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
export default visitBracketedSelection;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter selector visitor.
|
|
3
|
+
*
|
|
4
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535#section-2.3.5
|
|
5
|
+
*
|
|
6
|
+
* A filter selector [?expr] selects all children where the expression is true.
|
|
7
|
+
* For arrays: tests each element
|
|
8
|
+
* For objects: tests each member value
|
|
9
|
+
*/
|
|
10
|
+
import evaluateLogicalExpr from "../evaluators/logical-expr.mjs";
|
|
11
|
+
/**
|
|
12
|
+
* Visit a filter selector.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} ctx - Evaluation context
|
|
15
|
+
* @param {object} ctx.realm - Data realm
|
|
16
|
+
* @param {object} ctx.root - Root value ($)
|
|
17
|
+
* @param {unknown} value - Current value
|
|
18
|
+
* @param {object} node - AST node
|
|
19
|
+
* @param {object} node.expression - Logical expression to evaluate
|
|
20
|
+
* @param {(value: unknown, segment: string | number) => void} emit - Callback to emit selected value
|
|
21
|
+
*/
|
|
22
|
+
const visitFilterSelector = (ctx, value, node, emit) => {
|
|
23
|
+
const {
|
|
24
|
+
realm,
|
|
25
|
+
root
|
|
26
|
+
} = ctx;
|
|
27
|
+
const {
|
|
28
|
+
expression
|
|
29
|
+
} = node;
|
|
30
|
+
for (const [key, child] of realm.entries(value)) {
|
|
31
|
+
const result = evaluateLogicalExpr(ctx, root, child, expression);
|
|
32
|
+
if (result) {
|
|
33
|
+
emit(child, key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
export default visitFilterSelector;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index selector visitor.
|
|
3
|
+
*
|
|
4
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535#section-2.3.3
|
|
5
|
+
*
|
|
6
|
+
* An index selector selects at most one element from an array.
|
|
7
|
+
* Supports negative indices (count from end).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalize an array index.
|
|
12
|
+
* Negative indices count from the end.
|
|
13
|
+
*
|
|
14
|
+
* @param {number} index - The index (may be negative)
|
|
15
|
+
* @param {number} length - Array length
|
|
16
|
+
* @returns {number} - Normalized index (non-negative or out of bounds)
|
|
17
|
+
*/
|
|
18
|
+
const normalizeIndex = (index, length) => {
|
|
19
|
+
if (index >= 0) return index;
|
|
20
|
+
return length + index;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Visit an index selector.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} ctx - Evaluation context
|
|
27
|
+
* @param {object} ctx.realm - Data realm
|
|
28
|
+
* @param {unknown} value - Current value
|
|
29
|
+
* @param {object} node - AST node
|
|
30
|
+
* @param {number} node.value - Index to select
|
|
31
|
+
* @param {(value: unknown, segment: string | number) => void} emit - Callback to emit selected value
|
|
32
|
+
*/
|
|
33
|
+
const visitIndexSelector = (ctx, value, node, emit) => {
|
|
34
|
+
const {
|
|
35
|
+
realm
|
|
36
|
+
} = ctx;
|
|
37
|
+
const {
|
|
38
|
+
value: index
|
|
39
|
+
} = node;
|
|
40
|
+
if (!realm.isArray(value)) return;
|
|
41
|
+
const length = realm.getLength(value);
|
|
42
|
+
const normalizedIndex = normalizeIndex(index, length);
|
|
43
|
+
|
|
44
|
+
// Check bounds
|
|
45
|
+
if (normalizedIndex >= 0 && normalizedIndex < length) {
|
|
46
|
+
const selected = realm.getElement(value, normalizedIndex);
|
|
47
|
+
emit(selected, normalizedIndex);
|
|
48
|
+
}
|
|
49
|
+
// If out of bounds, yield nothing
|
|
50
|
+
};
|
|
51
|
+
export default visitIndexSelector;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name selector visitor.
|
|
3
|
+
*
|
|
4
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535#section-2.3.1
|
|
5
|
+
*
|
|
6
|
+
* A name selector selects at most one member value from an object.
|
|
7
|
+
* If the object has the specified member, yield its value.
|
|
8
|
+
* If not, yield nothing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Visit a name selector.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} ctx - Evaluation context
|
|
15
|
+
* @param {object} ctx.realm - Data realm
|
|
16
|
+
* @param {unknown} value - Current value
|
|
17
|
+
* @param {object} node - AST node
|
|
18
|
+
* @param {string} node.value - Property name to select
|
|
19
|
+
* @param {(value: unknown, segment: string | number) => void} emit - Callback to emit selected value
|
|
20
|
+
*/
|
|
21
|
+
const visitNameSelector = (ctx, value, node, emit) => {
|
|
22
|
+
const {
|
|
23
|
+
realm
|
|
24
|
+
} = ctx;
|
|
25
|
+
const {
|
|
26
|
+
value: name
|
|
27
|
+
} = node;
|
|
28
|
+
if (realm.isObject(value) && realm.hasProperty(value, name)) {
|
|
29
|
+
const selected = realm.getProperty(value, name);
|
|
30
|
+
emit(selected, name);
|
|
31
|
+
}
|
|
32
|
+
// If not an object or property doesn't exist, yield nothing
|
|
33
|
+
};
|
|
34
|
+
export default visitNameSelector;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Segment visitor dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Handles ChildSegment and DescendantSegment types.
|
|
5
|
+
*
|
|
6
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535#section-2.5
|
|
7
|
+
*/
|
|
8
|
+
import visitSelector from "./selector.mjs";
|
|
9
|
+
import visitBracketedSelection from "./bracketed-selection.mjs";
|
|
10
|
+
/**
|
|
11
|
+
* Visit a segment's selector.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} ctx - Evaluation context
|
|
14
|
+
* @param {unknown} value - Current value
|
|
15
|
+
* @param {object} selector - Selector AST node
|
|
16
|
+
* @param {(value: unknown, segment: string | number) => void} emit - Callback to emit selected value
|
|
17
|
+
*/
|
|
18
|
+
const visitSegmentSelector = (ctx, value, selector, emit) => {
|
|
19
|
+
switch (selector.type) {
|
|
20
|
+
case 'BracketedSelection':
|
|
21
|
+
visitBracketedSelection(ctx, value, selector, emit);
|
|
22
|
+
break;
|
|
23
|
+
case 'NameSelector':
|
|
24
|
+
case 'WildcardSelector':
|
|
25
|
+
case 'IndexSelector':
|
|
26
|
+
case 'SliceSelector':
|
|
27
|
+
case 'FilterSelector':
|
|
28
|
+
visitSelector(ctx, value, selector, emit);
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Visit a child segment.
|
|
37
|
+
* Applies selector to current value.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} ctx - Evaluation context
|
|
40
|
+
* @param {unknown} value - Current value
|
|
41
|
+
* @param {object} node - ChildSegment AST node
|
|
42
|
+
* @param {(value: unknown, segment: string | number) => void} emit - Callback to emit selected value
|
|
43
|
+
*/
|
|
44
|
+
export const visitChildSegment = (ctx, value, node, emit) => {
|
|
45
|
+
const {
|
|
46
|
+
selector
|
|
47
|
+
} = node;
|
|
48
|
+
visitSegmentSelector(ctx, value, selector, emit);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Visit a descendant segment.
|
|
53
|
+
* Applies selector to current value and all descendants.
|
|
54
|
+
*
|
|
55
|
+
* Note: This is used by exec.js which handles the recursive descent
|
|
56
|
+
* by pushing descendants onto the stack. This function only applies
|
|
57
|
+
* the selector at the current level.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} ctx - Evaluation context
|
|
60
|
+
* @param {unknown} value - Current value
|
|
61
|
+
* @param {object} node - DescendantSegment AST node
|
|
62
|
+
* @param {(value: unknown, segment: string | number) => void} emit - Callback to emit selected value
|
|
63
|
+
*/
|
|
64
|
+
export const visitDescendantSegment = (ctx, value, node, emit) => {
|
|
65
|
+
const {
|
|
66
|
+
selector
|
|
67
|
+
} = node;
|
|
68
|
+
visitSegmentSelector(ctx, value, selector, emit);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Visit a segment and emit selected values.
|
|
73
|
+
*
|
|
74
|
+
* @param {object} ctx - Evaluation context
|
|
75
|
+
* @param {unknown} value - Current value
|
|
76
|
+
* @param {object} node - Segment AST node
|
|
77
|
+
* @param {(value: unknown, segment: string | number) => void} emit - Callback to emit selected value
|
|
78
|
+
*/
|
|
79
|
+
const visitSegment = (ctx, value, node, emit) => {
|
|
80
|
+
switch (node.type) {
|
|
81
|
+
case 'ChildSegment':
|
|
82
|
+
visitChildSegment(ctx, value, node, emit);
|
|
83
|
+
break;
|
|
84
|
+
case 'DescendantSegment':
|
|
85
|
+
visitDescendantSegment(ctx, value, node, emit);
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
export default visitSegment;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selector visitor dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Routes to the appropriate selector visitor based on AST node type.
|
|
5
|
+
*/
|
|
6
|
+
import visitNameSelector from "./name-selector.mjs";
|
|
7
|
+
import visitIndexSelector from "./index-selector.mjs";
|
|
8
|
+
import visitWildcardSelector from "./wildcard-selector.mjs";
|
|
9
|
+
import visitSliceSelector from "./slice-selector.mjs";
|
|
10
|
+
import visitFilterSelector from "./filter-selector.mjs";
|
|
11
|
+
/**
|
|
12
|
+
* Visit a selector and emit selected values.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} ctx - Evaluation context
|
|
15
|
+
* @param {unknown} value - Current value
|
|
16
|
+
* @param {object} node - Selector AST node
|
|
17
|
+
* @param {(value: unknown, segment: string | number) => void} emit - Callback to emit selected value
|
|
18
|
+
*/
|
|
19
|
+
const visitSelector = (ctx, value, node, emit) => {
|
|
20
|
+
switch (node.type) {
|
|
21
|
+
case 'NameSelector':
|
|
22
|
+
visitNameSelector(ctx, value, node, emit);
|
|
23
|
+
break;
|
|
24
|
+
case 'IndexSelector':
|
|
25
|
+
visitIndexSelector(ctx, value, node, emit);
|
|
26
|
+
break;
|
|
27
|
+
case 'WildcardSelector':
|
|
28
|
+
visitWildcardSelector(ctx, value, node, emit);
|
|
29
|
+
break;
|
|
30
|
+
case 'SliceSelector':
|
|
31
|
+
visitSliceSelector(ctx, value, node, emit);
|
|
32
|
+
break;
|
|
33
|
+
case 'FilterSelector':
|
|
34
|
+
visitFilterSelector(ctx, value, node, emit);
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
// Unknown selector type, yield nothing
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
export default visitSelector;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice selector visitor.
|
|
3
|
+
*
|
|
4
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535#section-2.3.4
|
|
5
|
+
*
|
|
6
|
+
* A slice selector [start:end:step] selects elements from an array.
|
|
7
|
+
* - start: first index (default 0 for positive step, len-1 for negative)
|
|
8
|
+
* - end: upper bound (default len for positive step, -len-1 for negative)
|
|
9
|
+
* - step: step size (default 1, must not be 0)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Normalize a slice bound according to RFC 9535.
|
|
14
|
+
* @param {number} index - The bound value
|
|
15
|
+
* @param {number} length - Array length
|
|
16
|
+
* @returns {number} - Normalized bound
|
|
17
|
+
*/
|
|
18
|
+
const normalizeBound = (index, length) => {
|
|
19
|
+
if (index >= 0) {
|
|
20
|
+
return Math.min(index, length);
|
|
21
|
+
}
|
|
22
|
+
return Math.max(length + index, 0);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get slice bounds and step according to RFC 9535 Section 2.3.4.2.
|
|
27
|
+
*
|
|
28
|
+
* @param {number | null} start
|
|
29
|
+
* @param {number | null} end
|
|
30
|
+
* @param {number | null} step
|
|
31
|
+
* @param {number} length - Array length
|
|
32
|
+
* @returns {{ lower: number, upper: number, step: number } | null}
|
|
33
|
+
*/
|
|
34
|
+
const getSliceBounds = (start, end, step, length) => {
|
|
35
|
+
// Default step is 1
|
|
36
|
+
const actualStep = step ?? 1;
|
|
37
|
+
|
|
38
|
+
// Step of 0 is not allowed
|
|
39
|
+
if (actualStep === 0) return null;
|
|
40
|
+
let lower;
|
|
41
|
+
let upper;
|
|
42
|
+
if (actualStep > 0) {
|
|
43
|
+
// Forward iteration
|
|
44
|
+
const defaultStart = 0;
|
|
45
|
+
const defaultEnd = length;
|
|
46
|
+
const normalizedStart = start !== null ? normalizeBound(start, length) : defaultStart;
|
|
47
|
+
const normalizedEnd = end !== null ? normalizeBound(end, length) : defaultEnd;
|
|
48
|
+
lower = Math.max(normalizedStart, 0);
|
|
49
|
+
upper = Math.min(normalizedEnd, length);
|
|
50
|
+
} else {
|
|
51
|
+
// Backward iteration
|
|
52
|
+
const defaultStart = length - 1;
|
|
53
|
+
const defaultEnd = -length - 1;
|
|
54
|
+
const normalizedStart = start !== null ? start >= 0 ? Math.min(start, length - 1) : Math.max(length + start, -1) : defaultStart;
|
|
55
|
+
const normalizedEnd = end !== null ? end >= 0 ? Math.min(end, length - 1) : Math.max(length + end, -1) : defaultEnd;
|
|
56
|
+
upper = Math.min(normalizedStart, length - 1);
|
|
57
|
+
lower = Math.max(normalizedEnd, -1);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
lower,
|
|
61
|
+
upper,
|
|
62
|
+
step: actualStep
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Visit a slice selector.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} ctx - Evaluation context
|
|
70
|
+
* @param {object} ctx.realm - Data realm
|
|
71
|
+
* @param {unknown} value - Current value
|
|
72
|
+
* @param {object} node - AST node
|
|
73
|
+
* @param {number | null} node.start - Start index
|
|
74
|
+
* @param {number | null} node.end - End index
|
|
75
|
+
* @param {number | null} node.step - Step
|
|
76
|
+
* @param {(value: unknown, segment: string | number) => void} emit - Callback to emit selected value
|
|
77
|
+
*/
|
|
78
|
+
const visitSliceSelector = (ctx, value, node, emit) => {
|
|
79
|
+
const {
|
|
80
|
+
realm
|
|
81
|
+
} = ctx;
|
|
82
|
+
const {
|
|
83
|
+
start,
|
|
84
|
+
end,
|
|
85
|
+
step
|
|
86
|
+
} = node;
|
|
87
|
+
if (!realm.isArray(value)) return;
|
|
88
|
+
const length = realm.getLength(value);
|
|
89
|
+
const bounds = getSliceBounds(start, end, step, length);
|
|
90
|
+
if (bounds === null) return; // step was 0
|
|
91
|
+
|
|
92
|
+
const {
|
|
93
|
+
lower,
|
|
94
|
+
upper,
|
|
95
|
+
step: actualStep
|
|
96
|
+
} = bounds;
|
|
97
|
+
if (actualStep > 0) {
|
|
98
|
+
// Forward iteration
|
|
99
|
+
for (let i = lower; i < upper; i += actualStep) {
|
|
100
|
+
const selected = realm.getElement(value, i);
|
|
101
|
+
emit(selected, i);
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
// Backward iteration
|
|
105
|
+
for (let i = upper; i > lower; i += actualStep) {
|
|
106
|
+
const selected = realm.getElement(value, i);
|
|
107
|
+
emit(selected, i);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
export default visitSliceSelector;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wildcard selector visitor.
|
|
3
|
+
*
|
|
4
|
+
* @see https://www.rfc-editor.org/rfc/rfc9535#section-2.3.2
|
|
5
|
+
*
|
|
6
|
+
* A wildcard selector selects all children of a value:
|
|
7
|
+
* - For arrays: all elements
|
|
8
|
+
* - For objects: all member values
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Visit a wildcard selector.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} ctx - Evaluation context
|
|
15
|
+
* @param {object} ctx.realm - Data realm
|
|
16
|
+
* @param {unknown} value - Current value
|
|
17
|
+
* @param {object} node - AST node (unused for wildcard)
|
|
18
|
+
* @param {(value: unknown, segment: string | number) => void} emit - Callback to emit selected value
|
|
19
|
+
*/
|
|
20
|
+
const visitWildcardSelector = (ctx, value, node, emit) => {
|
|
21
|
+
const {
|
|
22
|
+
realm
|
|
23
|
+
} = ctx;
|
|
24
|
+
for (const [key, child] of realm.entries(value)) {
|
|
25
|
+
emit(child, key);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
export default visitWildcardSelector;
|
package/es/index.mjs
CHANGED
|
@@ -6,8 +6,12 @@ export { default as ASTTranslator } from "./parse/translators/ASTTranslator/inde
|
|
|
6
6
|
export { default as XMLTranslator } from "./parse/translators/XMLTranslator.mjs";
|
|
7
7
|
export { default as Trace } from "./parse/trace/Trace.mjs";
|
|
8
8
|
export { default as test } from "./test/index.mjs";
|
|
9
|
-
export
|
|
10
|
-
export { default as
|
|
9
|
+
export * as NormalizedPath from "./normalized-path.mjs";
|
|
10
|
+
export { default as evaluate } from "./evaluate/index.mjs";
|
|
11
|
+
export * as functions from "./evaluate/functions/index.mjs";
|
|
12
|
+
export { default as EvaluationRealm } from "./evaluate/realms/EvaluationRealm.mjs";
|
|
13
|
+
export { default as JSONEvaluationRealm } from "./evaluate/realms/json/index.mjs";
|
|
11
14
|
export { default as JSONPathError } from "./errors/JSONPathError.mjs";
|
|
12
15
|
export { default as JSONPathParseError } from "./errors/JSONPathParseError.mjs";
|
|
13
|
-
export { default as
|
|
16
|
+
export { default as JSONNormalizedPathError } from "./errors/JSONNormalizedPathError.mjs";
|
|
17
|
+
export { default as JSONPathEvaluateError } from "./errors/JSONPathEvaluateError.mjs";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import parse from "./parse/index.mjs";
|
|
2
|
+
import testFn from "./test/index.mjs";
|
|
3
|
+
import JSONNormalizedPathError from "./errors/JSONNormalizedPathError.mjs";
|
|
4
|
+
/**
|
|
5
|
+
* Tests if a string is a valid normalized JSONPath.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} normalizedPath - The string to test
|
|
8
|
+
* @returns {boolean} True if valid normalized path, false otherwise
|
|
9
|
+
*/
|
|
10
|
+
export const test = normalizedPath => testFn(normalizedPath, {
|
|
11
|
+
normalized: true
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Escapes a string for use in a normalized JSONPath name selector.
|
|
16
|
+
* Follows RFC 9535 Section 2.7 escaping rules for single-quoted strings.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} selector - The string to escape
|
|
19
|
+
* @returns {string} The escaped string (without surrounding quotes)
|
|
20
|
+
*/
|
|
21
|
+
export const escape = selector => {
|
|
22
|
+
if (typeof selector !== 'string') {
|
|
23
|
+
throw new TypeError('Selector must be a string');
|
|
24
|
+
}
|
|
25
|
+
let escaped = '';
|
|
26
|
+
for (const char of selector) {
|
|
27
|
+
const codePoint = char.codePointAt(0);
|
|
28
|
+
switch (codePoint) {
|
|
29
|
+
case 0x08:
|
|
30
|
+
// backspace
|
|
31
|
+
escaped += '\\b';
|
|
32
|
+
break;
|
|
33
|
+
case 0x09:
|
|
34
|
+
// horizontal tab
|
|
35
|
+
escaped += '\\t';
|
|
36
|
+
break;
|
|
37
|
+
case 0x0a:
|
|
38
|
+
// line feed
|
|
39
|
+
escaped += '\\n';
|
|
40
|
+
break;
|
|
41
|
+
case 0x0c:
|
|
42
|
+
// form feed
|
|
43
|
+
escaped += '\\f';
|
|
44
|
+
break;
|
|
45
|
+
case 0x0d:
|
|
46
|
+
// carriage return
|
|
47
|
+
escaped += '\\r';
|
|
48
|
+
break;
|
|
49
|
+
case 0x27:
|
|
50
|
+
// apostrophe '
|
|
51
|
+
escaped += "\\'";
|
|
52
|
+
break;
|
|
53
|
+
case 0x5c:
|
|
54
|
+
// backslash \
|
|
55
|
+
escaped += '\\\\';
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
// Other control characters (U+0000-U+001F except those handled above)
|
|
59
|
+
if (codePoint <= 0x1f) {
|
|
60
|
+
escaped += `\\u${codePoint.toString(16).padStart(4, '0')}`;
|
|
61
|
+
} else {
|
|
62
|
+
escaped += char;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return escaped;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates a normalized path string from a list of selectors.
|
|
71
|
+
* Name selectors are automatically escaped.
|
|
72
|
+
*
|
|
73
|
+
* @param {Array<string|number>} selectors - Array of name selectors (strings) or index selectors (numbers)
|
|
74
|
+
* @returns {string} A normalized JSONPath string
|
|
75
|
+
* @throws {JSONNormalizedPathError} If selectors is not an array or contains invalid selector types
|
|
76
|
+
*/
|
|
77
|
+
export const from = selectors => {
|
|
78
|
+
if (!Array.isArray(selectors)) {
|
|
79
|
+
throw new JSONNormalizedPathError(`Selectors must be an array, got: ${typeof selectors}`, {
|
|
80
|
+
selectors
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const segments = selectors.map(selector => {
|
|
85
|
+
if (typeof selector === 'string') {
|
|
86
|
+
// Name selector: escape and wrap in single quotes
|
|
87
|
+
return `['${escape(selector)}']`;
|
|
88
|
+
}
|
|
89
|
+
if (typeof selector === 'number') {
|
|
90
|
+
// Index selector: must be a non-negative safe integer (RFC 9535 Section 2.1)
|
|
91
|
+
if (!Number.isSafeInteger(selector) || selector < 0) {
|
|
92
|
+
throw new TypeError(`Index selector must be a non-negative safe integer, got: ${selector}`);
|
|
93
|
+
}
|
|
94
|
+
return `[${selector}]`;
|
|
95
|
+
}
|
|
96
|
+
throw new TypeError(`Selector must be a string or non-negative integer, got: ${typeof selector}`);
|
|
97
|
+
});
|
|
98
|
+
return `$${segments.join('')}`;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw new JSONNormalizedPathError('Failed to compile normalized JSONPath', {
|
|
101
|
+
cause: error,
|
|
102
|
+
selectors
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parses a normalized path string and returns a list of selectors.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} normalizedPath - A normalized JSONPath string
|
|
111
|
+
* @returns {Array<string|number>} Array of name selectors (strings) or index selectors (numbers)
|
|
112
|
+
* @throws {JSONNormalizedPathError} If the normalized path is invalid
|
|
113
|
+
*/
|
|
114
|
+
export const to = normalizedPath => {
|
|
115
|
+
if (typeof normalizedPath !== 'string') {
|
|
116
|
+
throw new JSONNormalizedPathError(`Normalized path must be a string, got: ${typeof normalizedPath}`, {
|
|
117
|
+
normalizedPath
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const parseResult = parse(normalizedPath, {
|
|
121
|
+
normalized: true
|
|
122
|
+
});
|
|
123
|
+
if (!parseResult.result.success) {
|
|
124
|
+
throw new JSONNormalizedPathError('Invalid normalized path', {
|
|
125
|
+
normalizedPath
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const {
|
|
129
|
+
tree
|
|
130
|
+
} = parseResult;
|
|
131
|
+
|
|
132
|
+
// Extract selectors from AST segments
|
|
133
|
+
// Normalized path grammar only allows NameSelector and IndexSelector
|
|
134
|
+
// For normalized paths, segment.selector is directly the selector (not BracketedSelection)
|
|
135
|
+
return tree.segments.map(segment => segment.selector.value);
|
|
136
|
+
};
|