@safeaccess/inline 0.1.1 → 0.1.3
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/.gitattributes +1 -1
- package/CHANGELOG.md +23 -5
- package/LICENSE +1 -1
- package/README.md +79 -21
- package/dist/accessors/abstract-accessor.d.ts +24 -10
- package/dist/accessors/abstract-accessor.js +21 -8
- package/dist/accessors/abstract-integration-accessor.d.ts +22 -0
- package/dist/accessors/abstract-integration-accessor.js +23 -0
- package/dist/accessors/formats/any-accessor.d.ts +10 -8
- package/dist/accessors/formats/any-accessor.js +9 -8
- package/dist/accessors/formats/array-accessor.d.ts +2 -0
- package/dist/accessors/formats/array-accessor.js +2 -0
- package/dist/accessors/formats/env-accessor.d.ts +2 -0
- package/dist/accessors/formats/env-accessor.js +2 -0
- package/dist/accessors/formats/ini-accessor.d.ts +2 -0
- package/dist/accessors/formats/ini-accessor.js +2 -0
- package/dist/accessors/formats/json-accessor.d.ts +2 -0
- package/dist/accessors/formats/json-accessor.js +2 -0
- package/dist/accessors/formats/ndjson-accessor.d.ts +2 -0
- package/dist/accessors/formats/ndjson-accessor.js +2 -0
- package/dist/accessors/formats/object-accessor.d.ts +2 -0
- package/dist/accessors/formats/object-accessor.js +2 -0
- package/dist/accessors/formats/xml-accessor.d.ts +2 -0
- package/dist/accessors/formats/xml-accessor.js +2 -0
- package/dist/accessors/formats/yaml-accessor.d.ts +3 -1
- package/dist/accessors/formats/yaml-accessor.js +4 -2
- package/dist/cache/simple-path-cache.d.ts +51 -0
- package/dist/cache/simple-path-cache.js +72 -0
- package/dist/contracts/accessors-interface.d.ts +2 -0
- package/dist/contracts/factory-accessors-interface.d.ts +2 -0
- package/dist/contracts/filter-evaluator-interface.d.ts +28 -0
- package/dist/contracts/filter-evaluator-interface.js +1 -0
- package/dist/contracts/parse-integration-interface.d.ts +2 -0
- package/dist/contracts/parser-interface.d.ts +92 -0
- package/dist/contracts/parser-interface.js +1 -0
- package/dist/contracts/path-cache-interface.d.ts +7 -6
- package/dist/contracts/readable-accessors-interface.d.ts +11 -6
- package/dist/contracts/security-guard-interface.d.ts +2 -0
- package/dist/contracts/security-parser-interface.d.ts +2 -0
- package/dist/contracts/validatable-parser-interface.d.ts +59 -0
- package/dist/contracts/validatable-parser-interface.js +1 -0
- package/dist/contracts/writable-accessors-interface.d.ts +5 -0
- package/dist/core/accessor-factory.d.ts +124 -0
- package/dist/core/accessor-factory.js +157 -0
- package/dist/core/dot-notation-parser.d.ts +34 -5
- package/dist/core/dot-notation-parser.js +51 -10
- package/dist/core/inline-builder-accessor.d.ts +82 -0
- package/dist/core/inline-builder-accessor.js +107 -0
- package/dist/exceptions/accessor-exception.d.ts +9 -0
- package/dist/exceptions/accessor-exception.js +9 -0
- package/dist/exceptions/invalid-format-exception.d.ts +5 -0
- package/dist/exceptions/invalid-format-exception.js +5 -0
- package/dist/exceptions/parser-exception.d.ts +4 -0
- package/dist/exceptions/parser-exception.js +4 -0
- package/dist/exceptions/path-not-found-exception.d.ts +4 -0
- package/dist/exceptions/path-not-found-exception.js +4 -0
- package/dist/exceptions/readonly-violation-exception.d.ts +4 -0
- package/dist/exceptions/readonly-violation-exception.js +4 -0
- package/dist/exceptions/security-exception.d.ts +6 -0
- package/dist/exceptions/security-exception.js +6 -0
- package/dist/exceptions/unsupported-type-exception.d.ts +4 -0
- package/dist/exceptions/unsupported-type-exception.js +4 -0
- package/dist/exceptions/yaml-parse-exception.d.ts +4 -0
- package/dist/exceptions/yaml-parse-exception.js +4 -0
- package/dist/index.js +2 -1
- package/dist/inline.d.ts +26 -56
- package/dist/inline.js +43 -111
- package/dist/parser/xml-parser.js +23 -10
- package/dist/parser/yaml-parser.d.ts +54 -7
- package/dist/parser/yaml-parser.js +268 -51
- package/dist/path-query/segment-filter-parser.d.ts +142 -0
- package/dist/path-query/segment-filter-parser.js +384 -0
- package/dist/path-query/segment-parser.d.ts +98 -0
- package/dist/path-query/segment-parser.js +283 -0
- package/dist/path-query/segment-path-resolver.d.ts +149 -0
- package/dist/path-query/segment-path-resolver.js +351 -0
- package/dist/path-query/segment-type.d.ts +85 -0
- package/dist/path-query/segment-type.js +35 -0
- package/dist/security/forbidden-keys.d.ts +2 -2
- package/dist/security/forbidden-keys.js +5 -5
- package/dist/security/security-guard.d.ts +4 -1
- package/dist/security/security-guard.js +7 -2
- package/dist/security/security-parser.d.ts +10 -1
- package/dist/security/security-parser.js +10 -1
- package/dist/type-format.d.ts +2 -0
- package/dist/type-format.js +2 -0
- package/package.json +11 -3
- package/src/accessors/abstract-accessor.ts +25 -19
- package/src/accessors/abstract-integration-accessor.ts +27 -0
- package/src/accessors/formats/any-accessor.ts +11 -11
- package/src/accessors/formats/array-accessor.ts +2 -0
- package/src/accessors/formats/env-accessor.ts +2 -0
- package/src/accessors/formats/ini-accessor.ts +2 -0
- package/src/accessors/formats/json-accessor.ts +2 -0
- package/src/accessors/formats/ndjson-accessor.ts +2 -0
- package/src/accessors/formats/object-accessor.ts +2 -0
- package/src/accessors/formats/xml-accessor.ts +2 -0
- package/src/accessors/formats/yaml-accessor.ts +4 -2
- package/src/cache/simple-path-cache.ts +77 -0
- package/src/contracts/accessors-interface.ts +2 -0
- package/src/contracts/factory-accessors-interface.ts +2 -0
- package/src/contracts/filter-evaluator-interface.ts +30 -0
- package/src/contracts/parse-integration-interface.ts +2 -0
- package/src/contracts/parser-interface.ts +114 -0
- package/src/contracts/path-cache-interface.ts +8 -6
- package/src/contracts/readable-accessors-interface.ts +11 -6
- package/src/contracts/security-guard-interface.ts +2 -0
- package/src/contracts/security-parser-interface.ts +2 -0
- package/src/contracts/validatable-parser-interface.ts +64 -0
- package/src/contracts/writable-accessors-interface.ts +5 -0
- package/src/core/accessor-factory.ts +173 -0
- package/src/core/dot-notation-parser.ts +74 -11
- package/src/core/inline-builder-accessor.ts +163 -0
- package/src/exceptions/accessor-exception.ts +9 -0
- package/src/exceptions/invalid-format-exception.ts +5 -0
- package/src/exceptions/parser-exception.ts +4 -0
- package/src/exceptions/path-not-found-exception.ts +4 -0
- package/src/exceptions/readonly-violation-exception.ts +4 -0
- package/src/exceptions/security-exception.ts +6 -0
- package/src/exceptions/unsupported-type-exception.ts +4 -0
- package/src/exceptions/yaml-parse-exception.ts +4 -0
- package/src/index.ts +3 -1
- package/src/inline.ts +46 -120
- package/src/parser/xml-parser.ts +31 -10
- package/src/parser/yaml-parser.ts +310 -45
- package/src/path-query/segment-filter-parser.ts +444 -0
- package/src/path-query/segment-parser.ts +321 -0
- package/src/path-query/segment-path-resolver.ts +521 -0
- package/src/path-query/segment-type.ts +82 -0
- package/src/security/forbidden-keys.ts +5 -5
- package/src/security/security-guard.ts +10 -2
- package/src/security/security-parser.ts +18 -3
- package/src/type-format.ts +2 -0
- package/stryker.config.json +8 -10
- package/tests/accessors/abstract-accessor.test.ts +217 -0
- package/tests/accessors/abstract-integration-accessor.test.ts +37 -0
- package/tests/accessors/formats/any-accessor.test.ts +57 -0
- package/tests/accessors/formats/array-accessor.test.ts +42 -0
- package/tests/accessors/formats/env-accessor.test.ts +103 -0
- package/tests/accessors/formats/ini-accessor.test.ts +186 -0
- package/tests/accessors/{json-accessor.test.ts → formats/json-accessor.test.ts} +6 -6
- package/tests/accessors/formats/ndjson-accessor.test.ts +49 -0
- package/tests/accessors/formats/object-accessor.test.ts +172 -0
- package/tests/accessors/formats/xml-accessor.test.ts +162 -0
- package/tests/accessors/formats/yaml-accessor.test.ts +36 -0
- package/tests/cache/simple-path-cache.test.ts +168 -0
- package/tests/core/accessor-factory.test.ts +157 -0
- package/tests/core/dot-notation-parser-edge-cases.test.ts +415 -0
- package/tests/core/dot-notation-parser.test.ts +0 -288
- package/tests/core/inline-builder-accessor.test.ts +114 -0
- package/tests/exceptions/accessor-exception.test.ts +28 -0
- package/tests/exceptions/invalid-format-exception.test.ts +31 -0
- package/tests/exceptions/path-not-found-exception.test.ts +33 -0
- package/tests/exceptions/readonly-violation-exception.test.ts +35 -0
- package/tests/exceptions/security-exception.test.ts +33 -0
- package/tests/exceptions/unsupported-type-exception.test.ts +33 -0
- package/tests/exceptions/yaml-parse-exception.test.ts +38 -0
- package/tests/mocks/fake-path-cache.ts +4 -3
- package/tests/parity-from.test.ts +118 -0
- package/tests/parity.test.ts +227 -10
- package/tests/parser/xml-parser-mutations.test.ts +579 -0
- package/tests/parser/xml-parser-scanner.test.ts +379 -0
- package/tests/parser/xml-parser.test.ts +17 -330
- package/tests/parser/yaml-parser-mutations.test.ts +750 -0
- package/tests/parser/yaml-parser.test.ts +844 -18
- package/tests/path-query/segment-filter-parser-mutations.test.ts +735 -0
- package/tests/path-query/segment-filter-parser.test.ts +1091 -0
- package/tests/path-query/segment-parser-mutations.test.ts +539 -0
- package/tests/path-query/segment-parser.test.ts +606 -0
- package/tests/path-query/segment-path-resolver-mutations.test.ts +626 -0
- package/tests/path-query/segment-path-resolver.test.ts +1009 -0
- package/tests/security/security-guard-advanced.test.ts +413 -0
- package/tests/security/security-guard-forbidden-keys.test.ts +87 -0
- package/tests/security/security-guard.test.ts +8 -479
- package/tests/security/security-parser.test.ts +18 -14
- package/vitest.config.ts +3 -3
- package/benchmarks/get.bench.ts +0 -26
- package/benchmarks/parse.bench.ts +0 -41
- package/tests/accessors/accessors.test.ts +0 -1017
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from 'vitest';
|
|
2
|
+
import { SegmentPathResolver } from '../../src/path-query/segment-path-resolver.js';
|
|
3
|
+
import { SegmentParser } from '../../src/path-query/segment-parser.js';
|
|
4
|
+
import { SegmentFilterParser } from '../../src/path-query/segment-filter-parser.js';
|
|
5
|
+
import { SecurityGuard } from '../../src/security/security-guard.js';
|
|
6
|
+
import { SegmentType } from '../../src/path-query/segment-type.js';
|
|
7
|
+
import type { Segment } from '../../src/path-query/segment-type.js';
|
|
8
|
+
import { SecurityException } from '../../src/exceptions/security-exception.js';
|
|
9
|
+
|
|
10
|
+
describe(SegmentPathResolver.name, () => {
|
|
11
|
+
let filterParser: SegmentFilterParser;
|
|
12
|
+
let segmentParser: SegmentParser;
|
|
13
|
+
let resolver: SegmentPathResolver;
|
|
14
|
+
let r: (data: Record<string, unknown>, path: string, defaultValue?: unknown) => unknown;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
const guard = new SecurityGuard();
|
|
18
|
+
filterParser = new SegmentFilterParser(guard);
|
|
19
|
+
segmentParser = new SegmentParser(filterParser);
|
|
20
|
+
resolver = new SegmentPathResolver(filterParser);
|
|
21
|
+
r = (
|
|
22
|
+
data: Record<string, unknown>,
|
|
23
|
+
path: string,
|
|
24
|
+
defaultValue: unknown = null,
|
|
25
|
+
): unknown => {
|
|
26
|
+
const segments = segmentParser.parseSegments(path);
|
|
27
|
+
return resolver.resolve(data, segments, 0, defaultValue, 100);
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe(`${SegmentPathResolver.name} > resolve basics`, () => {
|
|
32
|
+
it('returns the value for an existing key', () => {
|
|
33
|
+
const data = { name: 'Alice' };
|
|
34
|
+
|
|
35
|
+
expect(r(data, 'name')).toBe('Alice');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns the default when the key does not exist', () => {
|
|
39
|
+
const data = { name: 'Alice' };
|
|
40
|
+
|
|
41
|
+
expect(r(data, 'missing', 'fallback')).toBe('fallback');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('resolves a nested two-level path', () => {
|
|
45
|
+
const data = { user: { name: 'Alice' } };
|
|
46
|
+
|
|
47
|
+
expect(r(data, 'user.name')).toBe('Alice');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('resolves a nested three-level path', () => {
|
|
51
|
+
const data = { user: { address: { city: 'Porto Alegre' } } };
|
|
52
|
+
|
|
53
|
+
expect(r(data, 'user.address.city')).toBe('Porto Alegre');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns current value when segments list is empty', () => {
|
|
57
|
+
const data = { key: 'value' };
|
|
58
|
+
const result = resolver.resolve(data, [], 0, null, 100);
|
|
59
|
+
|
|
60
|
+
expect(result).toEqual(data);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('throws SecurityException when index exceeds maxDepth', () => {
|
|
64
|
+
const data = { a: 1 };
|
|
65
|
+
const segments: Segment[] = [{ type: SegmentType.Key, value: 'a' }];
|
|
66
|
+
|
|
67
|
+
expect(() => resolver.resolve(data, segments, 200, null, 100)).toThrow(
|
|
68
|
+
SecurityException,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe(`${SegmentPathResolver.name} > resolve wildcard`, () => {
|
|
74
|
+
it('expands all children with a wildcard', () => {
|
|
75
|
+
const data = { users: ['Alice', 'Bob', 'Carol'] };
|
|
76
|
+
|
|
77
|
+
expect(r(data, 'users.*')).toEqual(['Alice', 'Bob', 'Carol']);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('returns default for a wildcard on a non-array value', () => {
|
|
81
|
+
const data = { name: 'Alice' };
|
|
82
|
+
|
|
83
|
+
expect(r(data, 'name.*', 'default')).toBe('default');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('chains wildcard with a nested key', () => {
|
|
87
|
+
const data = { users: [{ name: 'Alice' }, { name: 'Bob' }] };
|
|
88
|
+
|
|
89
|
+
expect(r(data, 'users.*.name')).toEqual(['Alice', 'Bob']);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe(`${SegmentPathResolver.name} > resolve descent`, () => {
|
|
94
|
+
it('collects all values for a recursive descent key', () => {
|
|
95
|
+
const data = {
|
|
96
|
+
name: 'Alice',
|
|
97
|
+
friend: { name: 'Bob' },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const result = r(data, '..name') as unknown[];
|
|
101
|
+
|
|
102
|
+
expect(result).toContain('Alice');
|
|
103
|
+
expect(result).toContain('Bob');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('collects values for DescentMulti with multiple keys', () => {
|
|
107
|
+
const data = {
|
|
108
|
+
a: 1,
|
|
109
|
+
b: 2,
|
|
110
|
+
nested: { a: 10, b: 20 },
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const result = r(data, "..['a','b']") as unknown[];
|
|
114
|
+
|
|
115
|
+
expect(result).toContain(1);
|
|
116
|
+
expect(result).toContain(2);
|
|
117
|
+
expect(result).toContain(10);
|
|
118
|
+
expect(result).toContain(20);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('returns default when DescentMulti finds no keys', () => {
|
|
122
|
+
const data = { x: 1 };
|
|
123
|
+
|
|
124
|
+
const result = r(data, "..['missing1','missing2']", 'fallback');
|
|
125
|
+
|
|
126
|
+
expect(result).toBe('fallback');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe(`${SegmentPathResolver.name} > resolve filter`, () => {
|
|
131
|
+
it('filters array items that satisfy a condition', () => {
|
|
132
|
+
const data = {
|
|
133
|
+
users: [
|
|
134
|
+
{ name: 'Alice', age: 30 },
|
|
135
|
+
{ name: 'Bob', age: 17 },
|
|
136
|
+
{ name: 'Carol', age: 25 },
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const result = r(data, 'users[?age>18]') as Array<Record<string, unknown>>;
|
|
141
|
+
|
|
142
|
+
expect(result).toHaveLength(2);
|
|
143
|
+
expect(result[0].name).toBe('Alice');
|
|
144
|
+
expect(result[1].name).toBe('Carol');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('returns empty array when no items match the filter', () => {
|
|
148
|
+
const data = { users: [{ name: 'Alice', age: 10 }] };
|
|
149
|
+
|
|
150
|
+
const result = r(data, 'users[?age>100]') as unknown[];
|
|
151
|
+
|
|
152
|
+
expect(result).toHaveLength(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('returns default when the filter is applied to a non-array value', () => {
|
|
156
|
+
const data = { name: 'Alice' };
|
|
157
|
+
|
|
158
|
+
const result = r(data, 'name[?age>18]', 'fallback');
|
|
159
|
+
|
|
160
|
+
expect(result).toBe('fallback');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('chains filter with a key to access a field of filtered items', () => {
|
|
164
|
+
const data = {
|
|
165
|
+
users: [
|
|
166
|
+
{ name: 'Alice', active: true },
|
|
167
|
+
{ name: 'Bob', active: false },
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const result = r(data, 'users[?active==true].name');
|
|
172
|
+
|
|
173
|
+
expect(result).toEqual(['Alice']);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe(`${SegmentPathResolver.name} > resolve multi-key and multi-index`, () => {
|
|
178
|
+
it("selects multiple keys with ['a','b']", () => {
|
|
179
|
+
const data = { data: { a: 1, b: 2, c: 3 } };
|
|
180
|
+
|
|
181
|
+
const result = r(data, "data['a','b']");
|
|
182
|
+
|
|
183
|
+
expect(result).toEqual([1, 2]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('returns default for a multi-key miss', () => {
|
|
187
|
+
const data = { data: { a: 1 } };
|
|
188
|
+
|
|
189
|
+
const result = r(data, "data['missing']", 'fallback');
|
|
190
|
+
|
|
191
|
+
expect(result).toBe('fallback');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('selects multiple indices [0,2]', () => {
|
|
195
|
+
const data = { items: ['a', 'b', 'c', 'd'] };
|
|
196
|
+
|
|
197
|
+
const result = r(data, 'items[0,2]');
|
|
198
|
+
|
|
199
|
+
expect(result).toEqual(['a', 'c']);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('resolves a negative multi-index [-1] via MultiIndex segment', () => {
|
|
203
|
+
const segments: Segment[] = [
|
|
204
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
205
|
+
{ type: SegmentType.MultiIndex, indices: [-1] },
|
|
206
|
+
];
|
|
207
|
+
const data = { items: ['a', 'b', 'c'] };
|
|
208
|
+
|
|
209
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
210
|
+
|
|
211
|
+
expect(result).toEqual(['c']);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('returns default when multi-index is out of bounds', () => {
|
|
215
|
+
const data = { items: ['a'] };
|
|
216
|
+
|
|
217
|
+
const result = r(data, 'items[0,99]', 'fallback') as unknown[];
|
|
218
|
+
|
|
219
|
+
expect(result[1]).toBe('fallback');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('returns default for multi-key on a non-object', () => {
|
|
223
|
+
const segments: Segment[] = [{ type: SegmentType.MultiKey, keys: ['a', 'b'] }];
|
|
224
|
+
|
|
225
|
+
const result = resolver.resolve('not-an-array', segments, 0, 'fallback', 100);
|
|
226
|
+
|
|
227
|
+
expect(result).toBe('fallback');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('returns default for multi-index on a non-object', () => {
|
|
231
|
+
const segments: Segment[] = [{ type: SegmentType.MultiIndex, indices: [0, 1] }];
|
|
232
|
+
|
|
233
|
+
const result = resolver.resolve('not-an-array', segments, 0, 'fallback', 100);
|
|
234
|
+
|
|
235
|
+
expect(result).toBe('fallback');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe(`${SegmentPathResolver.name} > resolve slice`, () => {
|
|
240
|
+
it('slices an array [1:3]', () => {
|
|
241
|
+
const data = { items: ['a', 'b', 'c', 'd', 'e'] };
|
|
242
|
+
|
|
243
|
+
const result = r(data, 'items[1:3]');
|
|
244
|
+
|
|
245
|
+
expect(result).toEqual(['b', 'c']);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('slices with a step [0:6:2]', () => {
|
|
249
|
+
const data = { items: ['a', 'b', 'c', 'd', 'e', 'f'] };
|
|
250
|
+
|
|
251
|
+
const result = r(data, 'items[0:6:2]');
|
|
252
|
+
|
|
253
|
+
expect(result).toEqual(['a', 'c', 'e']);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('returns default for a slice on a non-object', () => {
|
|
257
|
+
const data = { name: 'Alice' };
|
|
258
|
+
|
|
259
|
+
const result = r(data, 'name[0:1]', 'fallback');
|
|
260
|
+
|
|
261
|
+
expect(result).toBe('fallback');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('returns empty array for an out-of-range slice', () => {
|
|
265
|
+
const data = { items: ['a', 'b'] };
|
|
266
|
+
|
|
267
|
+
const result = r(data, 'items[99:100]') as unknown[];
|
|
268
|
+
|
|
269
|
+
expect(result).toHaveLength(0);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe(`${SegmentPathResolver.name} > resolve projection`, () => {
|
|
274
|
+
it('projects specific fields from a map', () => {
|
|
275
|
+
const data = { user: { name: 'Alice', age: 30, password: 'secret' } };
|
|
276
|
+
|
|
277
|
+
const result = r(data, 'user.{name,age}') as Record<string, unknown>;
|
|
278
|
+
|
|
279
|
+
expect(result).toEqual({ name: 'Alice', age: 30 });
|
|
280
|
+
expect(result).not.toHaveProperty('password');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('projects fields with an alias', () => {
|
|
284
|
+
const data = { user: { name: 'Alice' } };
|
|
285
|
+
|
|
286
|
+
const result = r(data, 'user.{fullName: name}') as Record<string, unknown>;
|
|
287
|
+
|
|
288
|
+
expect(result).toHaveProperty('fullName');
|
|
289
|
+
expect(result.fullName).toBe('Alice');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('projects fields from a list of items', () => {
|
|
293
|
+
const data = {
|
|
294
|
+
users: [
|
|
295
|
+
{ name: 'Alice', age: 30 },
|
|
296
|
+
{ name: 'Bob', age: 25 },
|
|
297
|
+
],
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const result = r(data, 'users.{name}');
|
|
301
|
+
|
|
302
|
+
expect(result).toEqual([{ name: 'Alice' }, { name: 'Bob' }]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('sets projected field to null when source key is missing', () => {
|
|
306
|
+
const data = { user: { name: 'Alice' } };
|
|
307
|
+
|
|
308
|
+
const result = r(data, 'user.{name,missing}') as Record<string, unknown>;
|
|
309
|
+
|
|
310
|
+
expect(result.missing).toBeNull();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('returns default for a projection on a non-object', () => {
|
|
314
|
+
const data = { name: 'Alice' };
|
|
315
|
+
|
|
316
|
+
const result = r(data, 'name.{foo}', 'fallback');
|
|
317
|
+
|
|
318
|
+
expect(result).toBe('fallback');
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe(`${SegmentPathResolver.name} > resolve edge cases`, () => {
|
|
323
|
+
it('resolves further segments after a multi-key selection', () => {
|
|
324
|
+
const data = { users: { alice: { age: 30 }, bob: { age: 25 } } };
|
|
325
|
+
|
|
326
|
+
const segments: Segment[] = [
|
|
327
|
+
{ type: SegmentType.Key, value: 'users' },
|
|
328
|
+
{ type: SegmentType.MultiKey, keys: ['alice', 'bob'] },
|
|
329
|
+
{ type: SegmentType.Key, value: 'age' },
|
|
330
|
+
];
|
|
331
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
332
|
+
|
|
333
|
+
expect(result).toEqual([30, 25]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('resolves further segments after a multi-index selection', () => {
|
|
337
|
+
const data = { items: [{ name: 'a' }, { name: 'b' }, { name: 'c' }, { name: 'd' }] };
|
|
338
|
+
|
|
339
|
+
const segments: Segment[] = [
|
|
340
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
341
|
+
{ type: SegmentType.MultiIndex, indices: [0, 2] },
|
|
342
|
+
{ type: SegmentType.Key, value: 'name' },
|
|
343
|
+
];
|
|
344
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
345
|
+
|
|
346
|
+
expect(result).toEqual(['a', 'c']);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('adjusts a negative start index in a slice', () => {
|
|
350
|
+
const segments: Segment[] = [
|
|
351
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
352
|
+
{ type: SegmentType.Slice, start: -2, end: null, step: 1 },
|
|
353
|
+
];
|
|
354
|
+
const data = { items: ['a', 'b', 'c', 'd'] };
|
|
355
|
+
|
|
356
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
357
|
+
|
|
358
|
+
expect(result).toEqual(['c', 'd']);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('adjusts a negative end index in a slice', () => {
|
|
362
|
+
const segments: Segment[] = [
|
|
363
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
364
|
+
{ type: SegmentType.Slice, start: null, end: -2, step: 1 },
|
|
365
|
+
];
|
|
366
|
+
const data = { items: ['a', 'b', 'c', 'd'] };
|
|
367
|
+
|
|
368
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
369
|
+
|
|
370
|
+
expect(result).toEqual(['a', 'b']);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('iterates in reverse order with a negative step', () => {
|
|
374
|
+
const segments: Segment[] = [
|
|
375
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
376
|
+
{ type: SegmentType.Slice, start: null, end: null, step: -1 },
|
|
377
|
+
];
|
|
378
|
+
const data = { items: ['a', 'b', 'c', 'd'] };
|
|
379
|
+
|
|
380
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
381
|
+
|
|
382
|
+
expect(result).toEqual(['d', 'c', 'b', 'a']);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('applies further segments to each sliced item', () => {
|
|
386
|
+
const data = { items: [{ name: 'a' }, { name: 'b' }, { name: 'c' }, { name: 'd' }] };
|
|
387
|
+
|
|
388
|
+
const result = r(data, 'items[1:3].name');
|
|
389
|
+
|
|
390
|
+
expect(result).toEqual(['b', 'c']);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('projects null fields for non-object items in a list', () => {
|
|
394
|
+
const data = { items: ['scalar1', 'scalar2'] };
|
|
395
|
+
|
|
396
|
+
const result = r(data, 'items.{name}') as Array<Record<string, unknown>>;
|
|
397
|
+
|
|
398
|
+
expect(result[0].name).toBeNull();
|
|
399
|
+
expect(result[1].name).toBeNull();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('applies further segments after projecting a list of items', () => {
|
|
403
|
+
const segments: Segment[] = [
|
|
404
|
+
{ type: SegmentType.Key, value: 'users' },
|
|
405
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'name', source: 'name' }] },
|
|
406
|
+
{ type: SegmentType.Key, value: 'extra' },
|
|
407
|
+
];
|
|
408
|
+
const data = {
|
|
409
|
+
users: [
|
|
410
|
+
{ name: 'Alice', extra: 'x' },
|
|
411
|
+
{ name: 'Bob', extra: 'y' },
|
|
412
|
+
],
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
416
|
+
|
|
417
|
+
expect(result).toEqual([null, null]);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('applies further segments after projecting a single map', () => {
|
|
421
|
+
const segments: Segment[] = [
|
|
422
|
+
{ type: SegmentType.Key, value: 'user' },
|
|
423
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'name', source: 'name' }] },
|
|
424
|
+
{ type: SegmentType.Key, value: 'missing' },
|
|
425
|
+
];
|
|
426
|
+
const data = { user: { name: 'Alice', age: 30 } };
|
|
427
|
+
|
|
428
|
+
const result = resolver.resolve(data, segments, 0, 'fallback', 100);
|
|
429
|
+
|
|
430
|
+
expect(result).toBe('fallback');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('returns empty results from a descent on a non-object resolved value', () => {
|
|
434
|
+
const data = { name: 'Alice' };
|
|
435
|
+
|
|
436
|
+
const result = r(data, 'name..key');
|
|
437
|
+
|
|
438
|
+
expect(result).toEqual([]);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('resolves further segments after descent finds a matching key (scalar result)', () => {
|
|
442
|
+
const data = { config: { settings: { debug: true } } };
|
|
443
|
+
|
|
444
|
+
const result = r(data, '..settings.debug');
|
|
445
|
+
|
|
446
|
+
expect(result).toEqual([true]);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('spreads list results from descent followed by a wildcard segment', () => {
|
|
450
|
+
const data = { a: { targets: [1, 2, 3] } };
|
|
451
|
+
|
|
452
|
+
const result = r(data, '..targets[*]');
|
|
453
|
+
|
|
454
|
+
expect(result).toEqual([1, 2, 3]);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('resolves a wildcard on an object (non-array) as its values', () => {
|
|
458
|
+
const segments: Segment[] = [{ type: SegmentType.Wildcard }];
|
|
459
|
+
const data = { a: 1, b: 2 };
|
|
460
|
+
|
|
461
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
462
|
+
|
|
463
|
+
expect(result).toEqual([1, 2]);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('resolves a filter on an object (non-array) using its values', () => {
|
|
467
|
+
const data = {
|
|
468
|
+
items: { first: { name: 'Alice', age: 30 }, second: { name: 'Bob', age: 17 } },
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const result = r(data, 'items[?age>18]') as Array<Record<string, unknown>>;
|
|
472
|
+
|
|
473
|
+
expect(result).toHaveLength(1);
|
|
474
|
+
expect(result[0].name).toBe('Alice');
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('skips non-object items in a filter array', () => {
|
|
478
|
+
const data = {
|
|
479
|
+
items: [{ name: 'Alice', age: 30 }, 'scalar', null, { name: 'Bob', age: 17 }],
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const result = r(data, 'items[?age>18]') as Array<Record<string, unknown>>;
|
|
483
|
+
|
|
484
|
+
expect(result).toHaveLength(1);
|
|
485
|
+
expect(result[0].name).toBe('Alice');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('resolves a multi-index on an object using Object.values order', () => {
|
|
489
|
+
const segments: Segment[] = [{ type: SegmentType.MultiIndex, indices: [0] }];
|
|
490
|
+
const data = { a: 'first', b: 'second' };
|
|
491
|
+
|
|
492
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
493
|
+
|
|
494
|
+
expect(result).toEqual(['first']);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('resolves a slice on an object using Object.values', () => {
|
|
498
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 0, end: 1, step: null }];
|
|
499
|
+
const data = { a: 'first', b: 'second' };
|
|
500
|
+
|
|
501
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
502
|
+
|
|
503
|
+
expect(result).toEqual(['first']);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('falls back to empty string key when segment has no value property', () => {
|
|
507
|
+
const fakeSegment = { type: 'unknown-type' } as unknown as Segment;
|
|
508
|
+
const result = resolver.resolve({ '': 'found' }, [fakeSegment], 0, 'default', 100);
|
|
509
|
+
|
|
510
|
+
expect(result).toBe('found');
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('returns default when segmentAny reaches a segment without value and key is empty', () => {
|
|
514
|
+
const fakeSegment = { type: 'unknown-type' } as unknown as Segment;
|
|
515
|
+
const result = resolver.resolve({ x: 1 }, [fakeSegment], 0, 'default', 100);
|
|
516
|
+
|
|
517
|
+
expect(result).toBe('default');
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('returns default for multi-key when key is missing and has further segments', () => {
|
|
521
|
+
const segments: Segment[] = [
|
|
522
|
+
{ type: SegmentType.MultiKey, keys: ['missing'] },
|
|
523
|
+
{ type: SegmentType.Key, value: 'child' },
|
|
524
|
+
];
|
|
525
|
+
const data = { a: 1 };
|
|
526
|
+
|
|
527
|
+
const result = resolver.resolve(data, segments, 0, 'fallback', 100);
|
|
528
|
+
|
|
529
|
+
expect(result).toEqual(['fallback']);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('returns default for multi-index when index resolves to null', () => {
|
|
533
|
+
const segments: Segment[] = [
|
|
534
|
+
{ type: SegmentType.MultiIndex, indices: [5] },
|
|
535
|
+
{ type: SegmentType.Key, value: 'name' },
|
|
536
|
+
];
|
|
537
|
+
const data = ['a', 'b'];
|
|
538
|
+
|
|
539
|
+
const result = resolver.resolve(data, segments, 0, 'fallback', 100);
|
|
540
|
+
|
|
541
|
+
expect(result).toEqual(['fallback']);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('returns default for a negative multi-index out of bounds', () => {
|
|
545
|
+
const segments: Segment[] = [{ type: SegmentType.MultiIndex, indices: [-10] }];
|
|
546
|
+
const data = ['a', 'b'];
|
|
547
|
+
|
|
548
|
+
const result = resolver.resolve(data, segments, 0, 'fallback', 100);
|
|
549
|
+
|
|
550
|
+
expect(result).toEqual(['fallback']);
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
describe(`${SegmentPathResolver.name} > mutation boundary tests`, () => {
|
|
555
|
+
it('returns items directly when wildcard is the last segment', () => {
|
|
556
|
+
const segments: Segment[] = [
|
|
557
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
558
|
+
{ type: SegmentType.Wildcard },
|
|
559
|
+
];
|
|
560
|
+
const data = { items: [1, 2, 3] };
|
|
561
|
+
|
|
562
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
563
|
+
|
|
564
|
+
expect(result).toEqual([1, 2, 3]);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('resolves further segments after wildcard (nextIndex === segments.length - 1)', () => {
|
|
568
|
+
const segments: Segment[] = [
|
|
569
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
570
|
+
{ type: SegmentType.Wildcard },
|
|
571
|
+
{ type: SegmentType.Key, value: 'name' },
|
|
572
|
+
];
|
|
573
|
+
const data = { items: [{ name: 'a' }, { name: 'b' }] };
|
|
574
|
+
|
|
575
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
576
|
+
|
|
577
|
+
expect(result).toEqual(['a', 'b']);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('returns filtered items directly when filter is the last segment', () => {
|
|
581
|
+
const data = { items: [{ age: 10 }, { age: 30 }] };
|
|
582
|
+
|
|
583
|
+
const result = r(data, 'items[?age>18]') as Array<Record<string, unknown>>;
|
|
584
|
+
|
|
585
|
+
expect(result).toHaveLength(1);
|
|
586
|
+
expect(result[0]).toEqual({ age: 30 });
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('resolves further segments after filter when more segments follow', () => {
|
|
590
|
+
const data = {
|
|
591
|
+
items: [
|
|
592
|
+
{ age: 10, name: 'A' },
|
|
593
|
+
{ age: 30, name: 'B' },
|
|
594
|
+
],
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const result = r(data, 'items[?age>18].name');
|
|
598
|
+
|
|
599
|
+
expect(result).toEqual(['B']);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('returns multi-key values directly when multiKey is the last segment', () => {
|
|
603
|
+
const segments: Segment[] = [
|
|
604
|
+
{ type: SegmentType.Key, value: 'data' },
|
|
605
|
+
{ type: SegmentType.MultiKey, keys: ['x', 'y'] },
|
|
606
|
+
];
|
|
607
|
+
const data = { data: { x: 1, y: 2, z: 3 } };
|
|
608
|
+
|
|
609
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
610
|
+
|
|
611
|
+
expect(result).toEqual([1, 2]);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('resolves further segments after multi-key when more segments exist', () => {
|
|
615
|
+
const segments: Segment[] = [
|
|
616
|
+
{ type: SegmentType.Key, value: 'data' },
|
|
617
|
+
{ type: SegmentType.MultiKey, keys: ['a', 'b'] },
|
|
618
|
+
{ type: SegmentType.Key, value: 'val' },
|
|
619
|
+
];
|
|
620
|
+
const data = { data: { a: { val: 10 }, b: { val: 20 } } };
|
|
621
|
+
|
|
622
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
623
|
+
|
|
624
|
+
expect(result).toEqual([10, 20]);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('returns multi-index values directly when multiIndex is the last segment', () => {
|
|
628
|
+
const segments: Segment[] = [
|
|
629
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
630
|
+
{ type: SegmentType.MultiIndex, indices: [0, 2] },
|
|
631
|
+
];
|
|
632
|
+
const data = { items: ['a', 'b', 'c'] };
|
|
633
|
+
|
|
634
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
635
|
+
|
|
636
|
+
expect(result).toEqual(['a', 'c']);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('resolves further segments after multi-index when more segments exist', () => {
|
|
640
|
+
const segments: Segment[] = [
|
|
641
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
642
|
+
{ type: SegmentType.MultiIndex, indices: [0, 1] },
|
|
643
|
+
{ type: SegmentType.Key, value: 'name' },
|
|
644
|
+
];
|
|
645
|
+
const data = { items: [{ name: 'x' }, { name: 'y' }] };
|
|
646
|
+
|
|
647
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
648
|
+
|
|
649
|
+
expect(result).toEqual(['x', 'y']);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('returns sliced items directly when slice is the last segment', () => {
|
|
653
|
+
const segments: Segment[] = [
|
|
654
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
655
|
+
{ type: SegmentType.Slice, start: 0, end: 2, step: null },
|
|
656
|
+
];
|
|
657
|
+
const data = { items: ['a', 'b', 'c'] };
|
|
658
|
+
|
|
659
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
660
|
+
|
|
661
|
+
expect(result).toEqual(['a', 'b']);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('resolves further segments after slice when more segments exist', () => {
|
|
665
|
+
const data = { items: [{ name: 'a' }, { name: 'b' }, { name: 'c' }] };
|
|
666
|
+
|
|
667
|
+
const result = r(data, 'items[0:2].name');
|
|
668
|
+
|
|
669
|
+
expect(result).toEqual(['a', 'b']);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('returns projected list directly when projection is the last segment (array)', () => {
|
|
673
|
+
const segments: Segment[] = [
|
|
674
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
675
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'n', source: 'name' }] },
|
|
676
|
+
];
|
|
677
|
+
const data = { items: [{ name: 'a' }, { name: 'b' }] };
|
|
678
|
+
|
|
679
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
680
|
+
|
|
681
|
+
expect(result).toEqual([{ n: 'a' }, { n: 'b' }]);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('resolves further segments after projection on array when more segments exist', () => {
|
|
685
|
+
const segments: Segment[] = [
|
|
686
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
687
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'n', source: 'name' }] },
|
|
688
|
+
{ type: SegmentType.Key, value: 'n' },
|
|
689
|
+
];
|
|
690
|
+
const data = { items: [{ name: 'a' }, { name: 'b' }] };
|
|
691
|
+
|
|
692
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
693
|
+
|
|
694
|
+
expect(result).toEqual(['a', 'b']);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('returns projected map directly when projection is the last segment (object)', () => {
|
|
698
|
+
const segments: Segment[] = [
|
|
699
|
+
{ type: SegmentType.Key, value: 'user' },
|
|
700
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'n', source: 'name' }] },
|
|
701
|
+
];
|
|
702
|
+
const data = { user: { name: 'Alice', age: 30 } };
|
|
703
|
+
|
|
704
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
705
|
+
|
|
706
|
+
expect(result).toEqual({ n: 'Alice' });
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('resolves further segments after projection on object when more segments exist', () => {
|
|
710
|
+
const segments: Segment[] = [
|
|
711
|
+
{ type: SegmentType.Key, value: 'user' },
|
|
712
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'n', source: 'name' }] },
|
|
713
|
+
{ type: SegmentType.Key, value: 'n' },
|
|
714
|
+
];
|
|
715
|
+
const data = { user: { name: 'Alice', age: 30 } };
|
|
716
|
+
|
|
717
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
718
|
+
|
|
719
|
+
expect(result).toBe('Alice');
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it('skips non-object items during filter evaluation', () => {
|
|
723
|
+
const data = {
|
|
724
|
+
items: [42, 'str', null, { age: 30 }],
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
const result = r(data, 'items[?age>18]') as Array<Record<string, unknown>>;
|
|
728
|
+
|
|
729
|
+
expect(result).toHaveLength(1);
|
|
730
|
+
expect(result[0]).toEqual({ age: 30 });
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('clamps start to len when start >= len in a positive-step slice', () => {
|
|
734
|
+
const segments: Segment[] = [
|
|
735
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
736
|
+
{ type: SegmentType.Slice, start: 100, end: 200, step: 1 },
|
|
737
|
+
];
|
|
738
|
+
const data = { items: ['a', 'b', 'c'] };
|
|
739
|
+
|
|
740
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
741
|
+
|
|
742
|
+
expect(result).toEqual([]);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('clamps start to len when start equals len exactly', () => {
|
|
746
|
+
const segments: Segment[] = [
|
|
747
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
748
|
+
{ type: SegmentType.Slice, start: 3, end: 5, step: 1 },
|
|
749
|
+
];
|
|
750
|
+
const data = { items: ['a', 'b', 'c'] };
|
|
751
|
+
|
|
752
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
753
|
+
|
|
754
|
+
expect(result).toEqual([]);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it('clamps end to len when end > len', () => {
|
|
758
|
+
const segments: Segment[] = [
|
|
759
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
760
|
+
{ type: SegmentType.Slice, start: 0, end: 100, step: 1 },
|
|
761
|
+
];
|
|
762
|
+
const data = { items: ['a', 'b', 'c'] };
|
|
763
|
+
|
|
764
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
765
|
+
|
|
766
|
+
expect(result).toEqual(['a', 'b', 'c']);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it('does not clamp end when end equals len exactly', () => {
|
|
770
|
+
const segments: Segment[] = [
|
|
771
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
772
|
+
{ type: SegmentType.Slice, start: 0, end: 3, step: 1 },
|
|
773
|
+
];
|
|
774
|
+
const data = { items: ['a', 'b', 'c'] };
|
|
775
|
+
|
|
776
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
777
|
+
|
|
778
|
+
expect(result).toEqual(['a', 'b', 'c']);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('uses step > 0 path for a positive step and step < 0 path for negative', () => {
|
|
782
|
+
const segPos: Segment[] = [{ type: SegmentType.Slice, start: 0, end: 3, step: 1 }];
|
|
783
|
+
const segNeg: Segment[] = [
|
|
784
|
+
{ type: SegmentType.Slice, start: null, end: null, step: -1 },
|
|
785
|
+
];
|
|
786
|
+
const data = ['a', 'b', 'c'];
|
|
787
|
+
|
|
788
|
+
const resPos = resolver.resolve(data, segPos, 0, null, 100);
|
|
789
|
+
const resNeg = resolver.resolve(data, segNeg, 0, null, 100);
|
|
790
|
+
|
|
791
|
+
expect(resPos).toEqual(['a', 'b', 'c']);
|
|
792
|
+
expect(resNeg).toEqual(['c', 'b', 'a']);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it('defaults to a positive start when step > 0 and start is null', () => {
|
|
796
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: null, end: 2, step: 1 }];
|
|
797
|
+
const data = ['a', 'b', 'c'];
|
|
798
|
+
|
|
799
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
800
|
+
|
|
801
|
+
expect(result).toEqual(['a', 'b']);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it('defaults to len-1 start when step is negative and start is null', () => {
|
|
805
|
+
const segments: Segment[] = [
|
|
806
|
+
{ type: SegmentType.Slice, start: null, end: null, step: -1 },
|
|
807
|
+
];
|
|
808
|
+
const data = ['a', 'b', 'c'];
|
|
809
|
+
|
|
810
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
811
|
+
|
|
812
|
+
expect(result).toEqual(['c', 'b', 'a']);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('defaults to len end when step is positive and end is null', () => {
|
|
816
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 1, end: null, step: 1 }];
|
|
817
|
+
const data = ['a', 'b', 'c'];
|
|
818
|
+
|
|
819
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
820
|
+
|
|
821
|
+
expect(result).toEqual(['b', 'c']);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
it('defaults to -len-1 end when step is negative and end is null', () => {
|
|
825
|
+
const segments: Segment[] = [
|
|
826
|
+
{ type: SegmentType.Slice, start: 2, end: null, step: -1 },
|
|
827
|
+
];
|
|
828
|
+
const data = ['a', 'b', 'c'];
|
|
829
|
+
|
|
830
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
831
|
+
|
|
832
|
+
expect(result).toEqual(['c', 'b', 'a']);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it('returns empty descent results when current is not an object', () => {
|
|
836
|
+
const segments: Segment[] = [
|
|
837
|
+
{ type: SegmentType.Key, value: 'x' },
|
|
838
|
+
{ type: SegmentType.Descent, key: 'a' },
|
|
839
|
+
];
|
|
840
|
+
const data = { x: 'scalar' };
|
|
841
|
+
|
|
842
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
843
|
+
|
|
844
|
+
expect(result).toEqual([]);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('skips null children during descent traversal', () => {
|
|
848
|
+
const data = {
|
|
849
|
+
a: { name: 'found' },
|
|
850
|
+
b: null,
|
|
851
|
+
c: 'scalar',
|
|
852
|
+
d: { nested: { name: 'also found' } },
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
const result = r(data, '..name') as unknown[];
|
|
856
|
+
|
|
857
|
+
expect(result).toContain('found');
|
|
858
|
+
expect(result).toContain('also found');
|
|
859
|
+
expect(result).toHaveLength(2);
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('skips primitive children during descent traversal', () => {
|
|
863
|
+
const data = {
|
|
864
|
+
name: 'top',
|
|
865
|
+
x: 42,
|
|
866
|
+
y: true,
|
|
867
|
+
z: { name: 'deep' },
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
const result = r(data, '..name') as unknown[];
|
|
871
|
+
|
|
872
|
+
expect(result).toEqual(['top', 'deep']);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it('returns default for wildcard on a scalar value', () => {
|
|
876
|
+
const segments: Segment[] = [{ type: SegmentType.Wildcard }];
|
|
877
|
+
|
|
878
|
+
const result = resolver.resolve(42, segments, 0, 'default', 100);
|
|
879
|
+
|
|
880
|
+
expect(result).toBe('default');
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it('returns default for filter on a scalar value', () => {
|
|
884
|
+
const filterParser2 = new SegmentFilterParser(new SecurityGuard());
|
|
885
|
+
const filterExpr = filterParser2.parse('age>18');
|
|
886
|
+
const segments: Segment[] = [{ type: SegmentType.Filter, expression: filterExpr }];
|
|
887
|
+
|
|
888
|
+
const result = resolver.resolve('not-object', segments, 0, 'default', 100);
|
|
889
|
+
|
|
890
|
+
expect(result).toBe('default');
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it('returns default for slice on a scalar value', () => {
|
|
894
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 0, end: 1, step: null }];
|
|
895
|
+
|
|
896
|
+
const result = resolver.resolve('not-object', segments, 0, 'default', 100);
|
|
897
|
+
|
|
898
|
+
expect(result).toBe('default');
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it('returns default for projection on a scalar value', () => {
|
|
902
|
+
const segments: Segment[] = [
|
|
903
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'n', source: 'name' }] },
|
|
904
|
+
];
|
|
905
|
+
|
|
906
|
+
const result = resolver.resolve('not-object', segments, 0, 'default', 100);
|
|
907
|
+
|
|
908
|
+
expect(result).toBe('default');
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('does not recurse into non-object children during descent', () => {
|
|
912
|
+
const data = {
|
|
913
|
+
items: [1, 2, 'text', null],
|
|
914
|
+
nested: { name: 'got it' },
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
const result = r(data, '..name') as unknown[];
|
|
918
|
+
|
|
919
|
+
expect(result).toEqual(['got it']);
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it('descent on empty object returns empty results', () => {
|
|
923
|
+
const result = r({}, '..key');
|
|
924
|
+
|
|
925
|
+
expect(result).toEqual([]);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
it('projection non-object items produce null fields in array projection', () => {
|
|
929
|
+
const segments: Segment[] = [
|
|
930
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'x', source: 'x' }] },
|
|
931
|
+
];
|
|
932
|
+
const data = [null, 42, 'str'];
|
|
933
|
+
|
|
934
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
935
|
+
|
|
936
|
+
expect(result).toEqual([{ x: null }, { x: null }, { x: null }]);
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
it('slice with step=0 distinction: step >= 0 vs step > 0 matters', () => {
|
|
940
|
+
const segments: Segment[] = [
|
|
941
|
+
{ type: SegmentType.Slice, start: null, end: null, step: 1 },
|
|
942
|
+
];
|
|
943
|
+
const data = ['a', 'b', 'c'];
|
|
944
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
945
|
+
|
|
946
|
+
expect(result).toEqual(['a', 'b', 'c']);
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
it('resolves descent collecting scalar from matched key then recursing into children', () => {
|
|
950
|
+
const data = {
|
|
951
|
+
tag: 'root',
|
|
952
|
+
inner: {
|
|
953
|
+
tag: 'child',
|
|
954
|
+
},
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
const result = r(data, '..tag') as unknown[];
|
|
958
|
+
|
|
959
|
+
expect(result).toEqual(['root', 'child']);
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
it('clamps start to len with negative step when start exceeds array length', () => {
|
|
963
|
+
const segments: Segment[] = [
|
|
964
|
+
{ type: SegmentType.Slice, start: 100, end: null, step: -1 },
|
|
965
|
+
];
|
|
966
|
+
const data = ['a', 'b', 'c'];
|
|
967
|
+
|
|
968
|
+
const result = resolver.resolve(data, segments, 0, null, 100) as unknown[];
|
|
969
|
+
|
|
970
|
+
expect(result).toHaveLength(4);
|
|
971
|
+
expect(result[0]).toBeUndefined();
|
|
972
|
+
expect(result.slice(1)).toEqual(['c', 'b', 'a']);
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
it('clamps end to len when end exceeds array length with positive step', () => {
|
|
976
|
+
const segments: Segment[] = [
|
|
977
|
+
{ type: SegmentType.Key, value: 'items' },
|
|
978
|
+
{ type: SegmentType.Slice, start: 1, end: 999, step: 1 },
|
|
979
|
+
];
|
|
980
|
+
const data = { items: ['a', 'b', 'c'] };
|
|
981
|
+
|
|
982
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
983
|
+
|
|
984
|
+
expect(result).toEqual(['b', 'c']);
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it('does not clamp end when end equals len (end > len only, not >=)', () => {
|
|
988
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 0, end: 3, step: 1 }];
|
|
989
|
+
const data = ['a', 'b', 'c'];
|
|
990
|
+
|
|
991
|
+
const result = resolver.resolve(data, segments, 0, null, 100);
|
|
992
|
+
|
|
993
|
+
expect(result).toEqual(['a', 'b', 'c']);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
it('clamps start to len when start equals len exactly with negative step', () => {
|
|
997
|
+
const segments: Segment[] = [
|
|
998
|
+
{ type: SegmentType.Slice, start: 3, end: null, step: -1 },
|
|
999
|
+
];
|
|
1000
|
+
const data = ['a', 'b', 'c'];
|
|
1001
|
+
|
|
1002
|
+
const result = resolver.resolve(data, segments, 0, null, 100) as unknown[];
|
|
1003
|
+
|
|
1004
|
+
expect(result).toHaveLength(4);
|
|
1005
|
+
expect(result[0]).toBeUndefined();
|
|
1006
|
+
expect(result.slice(1)).toEqual(['c', 'b', 'a']);
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
});
|