@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,626 @@
|
|
|
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} mutation tests`, () => {
|
|
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('SecurityException message content', () => {
|
|
32
|
+
it('throws SecurityException with non-empty message', () => {
|
|
33
|
+
const segments: Segment[] = [{ type: SegmentType.Key, value: 'a' }];
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
resolver.resolve({}, segments, 200, null, 100);
|
|
37
|
+
expect.fail('Should have thrown');
|
|
38
|
+
} catch (e) {
|
|
39
|
+
expect(e).toBeInstanceOf(SecurityException);
|
|
40
|
+
expect((e as Error).message).not.toBe('');
|
|
41
|
+
expect((e as Error).message).toContain('200');
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('throws at exact boundary: index == maxDepth+1', () => {
|
|
46
|
+
const segments: Segment[] = [{ type: SegmentType.Key, value: 'a' }];
|
|
47
|
+
|
|
48
|
+
expect(() => resolver.resolve({}, segments, 101, null, 100)).toThrow(SecurityException);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('does not throw at exact maxDepth boundary', () => {
|
|
52
|
+
const segments: Segment[] = [];
|
|
53
|
+
|
|
54
|
+
const result = resolver.resolve({ a: 1 }, segments, 100, null, 100);
|
|
55
|
+
|
|
56
|
+
expect(result).toEqual({ a: 1 });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('segmentAny null checks', () => {
|
|
61
|
+
it('returns default when current is null', () => {
|
|
62
|
+
const segments: Segment[] = [{ type: SegmentType.Key, value: 'x' }];
|
|
63
|
+
|
|
64
|
+
expect(resolver.resolve(null, segments, 0, 'def', 100)).toBe('def');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns default when current is a primitive string', () => {
|
|
68
|
+
const segments: Segment[] = [{ type: SegmentType.Key, value: 'x' }];
|
|
69
|
+
|
|
70
|
+
expect(resolver.resolve('text' as unknown, segments, 0, 'def', 100)).toBe('def');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns default when current is undefined', () => {
|
|
74
|
+
const segments: Segment[] = [{ type: SegmentType.Key, value: 'x' }];
|
|
75
|
+
|
|
76
|
+
expect(resolver.resolve(undefined as unknown, segments, 0, 'def', 100)).toBe('def');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('wildcard null checks', () => {
|
|
81
|
+
it('returns default when wildcard applied to null', () => {
|
|
82
|
+
const segments: Segment[] = [
|
|
83
|
+
{ type: SegmentType.Key, value: 'x' },
|
|
84
|
+
{ type: SegmentType.Wildcard },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
expect(resolver.resolve({ x: null }, segments, 0, 'def', 100)).toBe('def');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns default when wildcard applied to a number', () => {
|
|
91
|
+
const segments: Segment[] = [
|
|
92
|
+
{ type: SegmentType.Key, value: 'x' },
|
|
93
|
+
{ type: SegmentType.Wildcard },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
expect(resolver.resolve({ x: 42 }, segments, 0, 'def', 100)).toBe('def');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('filter terminal vs chained', () => {
|
|
101
|
+
it('returns filtered items when filter is the last segment', () => {
|
|
102
|
+
const data = { items: [{ a: 1 }, { a: 2 }, { a: 3 }] };
|
|
103
|
+
|
|
104
|
+
const result = r(data, 'items[?a>1]') as unknown[];
|
|
105
|
+
|
|
106
|
+
expect(result).toEqual([{ a: 2 }, { a: 3 }]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('resolves further segments on each filtered item', () => {
|
|
110
|
+
const data = {
|
|
111
|
+
items: [
|
|
112
|
+
{ a: 1, b: 'x' },
|
|
113
|
+
{ a: 2, b: 'y' },
|
|
114
|
+
{ a: 3, b: 'z' },
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const result = r(data, 'items[?a>1].b');
|
|
119
|
+
|
|
120
|
+
expect(result).toEqual(['y', 'z']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('returns default when filter applied to null', () => {
|
|
124
|
+
const segments: Segment[] = [
|
|
125
|
+
{ type: SegmentType.Key, value: 'x' },
|
|
126
|
+
{
|
|
127
|
+
type: SegmentType.Filter,
|
|
128
|
+
expression: {
|
|
129
|
+
conditions: [{ field: 'a', operator: '>', value: 0 }],
|
|
130
|
+
logicals: [],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
expect(resolver.resolve({ x: null }, segments, 0, 'def', 100)).toBe('def');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('multiKey terminal vs chained', () => {
|
|
140
|
+
it('returns values directly when multiKey is the last segment', () => {
|
|
141
|
+
const segments: Segment[] = [{ type: SegmentType.MultiKey, keys: ['a', 'b'] }];
|
|
142
|
+
|
|
143
|
+
const result = resolver.resolve({ a: 1, b: 2 }, segments, 0, null, 100);
|
|
144
|
+
|
|
145
|
+
expect(result).toEqual([1, 2]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('resolves further segments when multiKey is not the last segment', () => {
|
|
149
|
+
const segments: Segment[] = [
|
|
150
|
+
{ type: SegmentType.MultiKey, keys: ['x', 'y'] },
|
|
151
|
+
{ type: SegmentType.Key, value: 'name' },
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
const result = resolver.resolve(
|
|
155
|
+
{ x: { name: 'A' }, y: { name: 'B' } },
|
|
156
|
+
segments,
|
|
157
|
+
0,
|
|
158
|
+
null,
|
|
159
|
+
100,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(result).toEqual(['A', 'B']);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('returns default for multiKey on null', () => {
|
|
166
|
+
const segments: Segment[] = [
|
|
167
|
+
{ type: SegmentType.Key, value: 'z' },
|
|
168
|
+
{ type: SegmentType.MultiKey, keys: ['a'] },
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
expect(resolver.resolve({ z: null }, segments, 0, 'def', 100)).toBe('def');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('multiIndex terminal vs chained', () => {
|
|
176
|
+
it('returns values directly when multiIndex is the last segment', () => {
|
|
177
|
+
const segments: Segment[] = [{ type: SegmentType.MultiIndex, indices: [0, 2] }];
|
|
178
|
+
|
|
179
|
+
const result = resolver.resolve(['a', 'b', 'c'], segments, 0, null, 100);
|
|
180
|
+
|
|
181
|
+
expect(result).toEqual(['a', 'c']);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('resolves further segments when multiIndex is not the last', () => {
|
|
185
|
+
const segments: Segment[] = [
|
|
186
|
+
{ type: SegmentType.MultiIndex, indices: [0, 1] },
|
|
187
|
+
{ type: SegmentType.Key, value: 'v' },
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const result = resolver.resolve([{ v: 1 }, { v: 2 }], segments, 0, null, 100);
|
|
191
|
+
|
|
192
|
+
expect(result).toEqual([1, 2]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('returns default for multiIndex on null', () => {
|
|
196
|
+
const segments: Segment[] = [
|
|
197
|
+
{ type: SegmentType.Key, value: 'z' },
|
|
198
|
+
{ type: SegmentType.MultiIndex, indices: [0] },
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
expect(resolver.resolve({ z: null }, segments, 0, 'def', 100)).toBe('def');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('slice edge cases', () => {
|
|
206
|
+
it('returns sliced items when slice is the last segment', () => {
|
|
207
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 0, end: 2, step: null }];
|
|
208
|
+
|
|
209
|
+
expect(resolver.resolve(['a', 'b', 'c'], segments, 0, null, 100)).toEqual(['a', 'b']);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('resolves further segments on sliced items', () => {
|
|
213
|
+
const segments: Segment[] = [
|
|
214
|
+
{ type: SegmentType.Slice, start: 0, end: 2, step: null },
|
|
215
|
+
{ type: SegmentType.Key, value: 'n' },
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
expect(
|
|
219
|
+
resolver.resolve([{ n: 1 }, { n: 2 }, { n: 3 }], segments, 0, null, 100),
|
|
220
|
+
).toEqual([1, 2]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('returns default for slice on null', () => {
|
|
224
|
+
const segments: Segment[] = [
|
|
225
|
+
{ type: SegmentType.Key, value: 'z' },
|
|
226
|
+
{ type: SegmentType.Slice, start: 0, end: 1, step: null },
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
expect(resolver.resolve({ z: null }, segments, 0, 'def', 100)).toBe('def');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('clamps negative start to 0', () => {
|
|
233
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: -100, end: 2, step: 1 }];
|
|
234
|
+
|
|
235
|
+
expect(resolver.resolve(['a', 'b', 'c'], segments, 0, null, 100)).toEqual(['a', 'b']);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('clamps end larger than len to len', () => {
|
|
239
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 0, end: 100, step: 1 }];
|
|
240
|
+
|
|
241
|
+
expect(resolver.resolve(['a', 'b'], segments, 0, null, 100)).toEqual(['a', 'b']);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('clamps start larger than len to len (empty result)', () => {
|
|
245
|
+
const segments: Segment[] = [
|
|
246
|
+
{ type: SegmentType.Slice, start: 100, end: 200, step: 1 },
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
expect(resolver.resolve(['a', 'b'], segments, 0, null, 100)).toEqual([]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('negative end converts correctly', () => {
|
|
253
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 0, end: -1, step: 1 }];
|
|
254
|
+
|
|
255
|
+
expect(resolver.resolve(['a', 'b', 'c'], segments, 0, null, 100)).toEqual(['a', 'b']);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('uses correct default start for positive step (0)', () => {
|
|
259
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: null, end: 2, step: 1 }];
|
|
260
|
+
|
|
261
|
+
expect(resolver.resolve(['a', 'b', 'c'], segments, 0, null, 100)).toEqual(['a', 'b']);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('uses correct default start for negative step (len-1)', () => {
|
|
265
|
+
const segments: Segment[] = [
|
|
266
|
+
{ type: SegmentType.Slice, start: null, end: null, step: -1 },
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
expect(resolver.resolve(['a', 'b', 'c'], segments, 0, null, 100)).toEqual([
|
|
270
|
+
'c',
|
|
271
|
+
'b',
|
|
272
|
+
'a',
|
|
273
|
+
]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('uses correct default end for positive step (len)', () => {
|
|
277
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 0, end: null, step: 1 }];
|
|
278
|
+
|
|
279
|
+
expect(resolver.resolve(['a', 'b'], segments, 0, null, 100)).toEqual(['a', 'b']);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('uses correct default end for negative step (-len-1)', () => {
|
|
283
|
+
const segments: Segment[] = [
|
|
284
|
+
{ type: SegmentType.Slice, start: null, end: null, step: -2 },
|
|
285
|
+
];
|
|
286
|
+
const result = resolver.resolve(['a', 'b', 'c', 'd'], segments, 0, null, 100);
|
|
287
|
+
|
|
288
|
+
expect(result).toEqual(['d', 'b']);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('zero end stays zero for positive step (not mutated to <=0)', () => {
|
|
292
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 0, end: 0, step: 1 }];
|
|
293
|
+
|
|
294
|
+
expect(resolver.resolve(['a', 'b'], segments, 0, null, 100)).toEqual([]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('step of exactly 0 is rejected by parser', () => {
|
|
298
|
+
expect(() => segmentParser.parseSegments('[0:5:0]')).toThrow();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('projection edge cases', () => {
|
|
303
|
+
it('returns projected array when projection is last segment', () => {
|
|
304
|
+
const segments: Segment[] = [
|
|
305
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'x', source: 'a' }] },
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
const result = resolver.resolve([{ a: 1 }, { a: 2 }], segments, 0, null, 100);
|
|
309
|
+
|
|
310
|
+
expect(result).toEqual([{ x: 1 }, { x: 2 }]);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('resolves further segments after array projection', () => {
|
|
314
|
+
const segments: Segment[] = [
|
|
315
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'x', source: 'a' }] },
|
|
316
|
+
{ type: SegmentType.Key, value: 'x' },
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
const result = resolver.resolve([{ a: 1 }, { a: 2 }], segments, 0, null, 100);
|
|
320
|
+
|
|
321
|
+
expect(result).toEqual([1, 2]);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('returns projected object when projection on single object is last', () => {
|
|
325
|
+
const segments: Segment[] = [
|
|
326
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'x', source: 'a' }] },
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
const result = resolver.resolve({ a: 1 }, segments, 0, null, 100);
|
|
330
|
+
|
|
331
|
+
expect(result).toEqual({ x: 1 });
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('resolves further segments after single object projection', () => {
|
|
335
|
+
const segments: Segment[] = [
|
|
336
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'x', source: 'a' }] },
|
|
337
|
+
{ type: SegmentType.Key, value: 'x' },
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
const result = resolver.resolve({ a: 1 }, segments, 0, null, 100);
|
|
341
|
+
|
|
342
|
+
expect(result).toBe(1);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('returns default for projection on a primitive', () => {
|
|
346
|
+
const segments: Segment[] = [
|
|
347
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'x', source: 'a' }] },
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
expect(resolver.resolve('text' as unknown, segments, 0, 'def', 100)).toBe('def');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('sets null for non-object items in projected array', () => {
|
|
354
|
+
const segments: Segment[] = [
|
|
355
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'x', source: 'a' }] },
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
const result = resolver.resolve([null, 42, 'text'], segments, 0, null, 100);
|
|
359
|
+
|
|
360
|
+
expect(result).toEqual([{ x: null }, { x: null }, { x: null }]);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('handles projection on object with null current check', () => {
|
|
364
|
+
const segments: Segment[] = [
|
|
365
|
+
{ type: SegmentType.Key, value: 'z' },
|
|
366
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'x', source: 'a' }] },
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
expect(resolver.resolve({ z: null }, segments, 0, 'def', 100)).toBe('def');
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe('collectDescent edge cases', () => {
|
|
374
|
+
it('skips null children during descent', () => {
|
|
375
|
+
const data = {
|
|
376
|
+
a: { name: 'found' },
|
|
377
|
+
b: null,
|
|
378
|
+
c: { name: 'also found' },
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const result = r(data, '..name') as unknown[];
|
|
382
|
+
|
|
383
|
+
expect(result).toContain('found');
|
|
384
|
+
expect(result).toContain('also found');
|
|
385
|
+
expect(result).toHaveLength(2);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('skips primitive children during descent', () => {
|
|
389
|
+
const data = {
|
|
390
|
+
a: { name: 'found' },
|
|
391
|
+
b: 42,
|
|
392
|
+
c: 'text',
|
|
393
|
+
d: true,
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const result = r(data, '..name') as unknown[];
|
|
397
|
+
|
|
398
|
+
expect(result).toEqual(['found']);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('returns empty array when descent starts on a primitive', () => {
|
|
402
|
+
const segments: Segment[] = [{ type: SegmentType.Descent, key: 'x' }];
|
|
403
|
+
|
|
404
|
+
const result = resolver.resolve('primitive' as unknown, segments, 0, null, 100);
|
|
405
|
+
|
|
406
|
+
expect(result).toEqual([]);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('returns empty array when descent starts on null', () => {
|
|
410
|
+
const segments: Segment[] = [{ type: SegmentType.Descent, key: 'x' }];
|
|
411
|
+
|
|
412
|
+
const result = resolver.resolve(null, segments, 0, null, 100);
|
|
413
|
+
|
|
414
|
+
expect(result).toEqual([]);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('collects array values from descent into results', () => {
|
|
418
|
+
const data = {
|
|
419
|
+
l1: { items: [1, 2] },
|
|
420
|
+
l2: { items: [3, 4] },
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const result = r(data, '..items') as unknown[];
|
|
424
|
+
|
|
425
|
+
expect(result).toHaveLength(2);
|
|
426
|
+
expect(result[0]).toEqual([1, 2]);
|
|
427
|
+
expect(result[1]).toEqual([3, 4]);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('descentMulti edge cases', () => {
|
|
432
|
+
it('returns default when no keys are found', () => {
|
|
433
|
+
const segments: Segment[] = [
|
|
434
|
+
{ type: SegmentType.DescentMulti, keys: ['missing1', 'missing2'] },
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
const result = resolver.resolve({ a: 1 }, segments, 0, 'def', 100);
|
|
438
|
+
|
|
439
|
+
expect(result).toBe('def');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('returns results array when keys are found', () => {
|
|
443
|
+
const segments: Segment[] = [{ type: SegmentType.DescentMulti, keys: ['a', 'b'] }];
|
|
444
|
+
|
|
445
|
+
const result = resolver.resolve(
|
|
446
|
+
{ a: 1, b: 2, nested: { a: 10 } },
|
|
447
|
+
segments,
|
|
448
|
+
0,
|
|
449
|
+
null,
|
|
450
|
+
100,
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
expect(result).toEqual([1, 10, 2]);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
describe('filter on object values', () => {
|
|
458
|
+
it('applies filter to object values (not just arrays)', () => {
|
|
459
|
+
const data = {
|
|
460
|
+
items: { x: { score: 5 }, y: { score: 15 }, z: { score: 25 } },
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const result = r(data, 'items[?score>10]') as unknown[];
|
|
464
|
+
|
|
465
|
+
expect(result).toHaveLength(2);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('skips non-object items during filter evaluation', () => {
|
|
469
|
+
const data = { items: [{ a: 1 }, 'string', null, { a: 2 }] };
|
|
470
|
+
|
|
471
|
+
const result = r(data, 'items[?a>0]') as unknown[];
|
|
472
|
+
|
|
473
|
+
expect(result).toEqual([{ a: 1 }, { a: 2 }]);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
describe('slice - step boundary conditions', () => {
|
|
478
|
+
it('differentiates step > 0 from step >= 0 (positive step)', () => {
|
|
479
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 0, end: 3, step: 2 }];
|
|
480
|
+
|
|
481
|
+
const result = resolver.resolve(['a', 'b', 'c', 'd'], segments, 0, null, 100);
|
|
482
|
+
|
|
483
|
+
expect(result).toEqual(['a', 'c']);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('negative step iterates in reverse correctly', () => {
|
|
487
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 3, end: 0, step: -1 }];
|
|
488
|
+
|
|
489
|
+
const result = resolver.resolve(['a', 'b', 'c', 'd'], segments, 0, null, 100);
|
|
490
|
+
|
|
491
|
+
expect(result).toEqual(['d', 'c', 'b']);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('negative step with default start and end covers all items in reverse', () => {
|
|
495
|
+
const segments: Segment[] = [
|
|
496
|
+
{ type: SegmentType.Slice, start: null, end: null, step: -1 },
|
|
497
|
+
];
|
|
498
|
+
|
|
499
|
+
const result = resolver.resolve(['a', 'b', 'c'], segments, 0, null, 100);
|
|
500
|
+
|
|
501
|
+
expect(result).toEqual(['c', 'b', 'a']);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('step > 0 determines default start as 0 (not len-1)', () => {
|
|
505
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: null, end: 2, step: 1 }];
|
|
506
|
+
|
|
507
|
+
const result = resolver.resolve(['a', 'b', 'c'], segments, 0, null, 100);
|
|
508
|
+
|
|
509
|
+
expect(result).toEqual(['a', 'b']);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('step < 0 determines default start as len-1', () => {
|
|
513
|
+
const segments: Segment[] = [
|
|
514
|
+
{ type: SegmentType.Slice, start: null, end: 0, step: -1 },
|
|
515
|
+
];
|
|
516
|
+
|
|
517
|
+
const result = resolver.resolve(['a', 'b', 'c'], segments, 0, null, 100);
|
|
518
|
+
|
|
519
|
+
expect(result).toEqual(['c', 'b']);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('step > 0 uses < comparison (not <=) for slicing', () => {
|
|
523
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 0, end: 2, step: 1 }];
|
|
524
|
+
|
|
525
|
+
const result = resolver.resolve(['a', 'b', 'c'], segments, 0, null, 100);
|
|
526
|
+
|
|
527
|
+
expect(result).toEqual(['a', 'b']);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('step < 0 uses > comparison (not >=) for slicing', () => {
|
|
531
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 2, end: 0, step: -1 }];
|
|
532
|
+
|
|
533
|
+
const result = resolver.resolve(['a', 'b', 'c'], segments, 0, null, 100);
|
|
534
|
+
|
|
535
|
+
expect(result).toEqual(['c', 'b']);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
describe('slice - start/end clamping boundary', () => {
|
|
540
|
+
it('clamps start at len exactly (not start > len)', () => {
|
|
541
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 2, end: 5, step: 1 }];
|
|
542
|
+
|
|
543
|
+
const result = resolver.resolve(['a', 'b'], segments, 0, null, 100);
|
|
544
|
+
|
|
545
|
+
expect(result).toEqual([]);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('clamps end at len exactly (not end >= len)', () => {
|
|
549
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 0, end: 3, step: 1 }];
|
|
550
|
+
|
|
551
|
+
const result = resolver.resolve(['a', 'b'], segments, 0, null, 100);
|
|
552
|
+
|
|
553
|
+
expect(result).toEqual(['a', 'b']);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('does not clamp start at len-1 (clamps at len)', () => {
|
|
557
|
+
const segments: Segment[] = [{ type: SegmentType.Slice, start: 3, end: 5, step: 1 }];
|
|
558
|
+
|
|
559
|
+
const result = resolver.resolve(['a', 'b', 'c'], segments, 0, null, 100);
|
|
560
|
+
|
|
561
|
+
expect(result).toEqual([]);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
describe('projection - null item handling', () => {
|
|
566
|
+
it('sets null for missing source fields in projected object', () => {
|
|
567
|
+
const segments: Segment[] = [
|
|
568
|
+
{ type: SegmentType.Projection, fields: [{ alias: 'x', source: 'missing' }] },
|
|
569
|
+
];
|
|
570
|
+
|
|
571
|
+
const result = resolver.resolve({ a: 1 }, segments, 0, null, 100);
|
|
572
|
+
|
|
573
|
+
expect(result).toEqual({ x: null });
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
describe('collectDescent - child type checking', () => {
|
|
578
|
+
it('only descends into object children, not null', () => {
|
|
579
|
+
const data = {
|
|
580
|
+
a: { name: 'found' },
|
|
581
|
+
b: null,
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const result = r(data, '..name') as unknown[];
|
|
585
|
+
|
|
586
|
+
expect(result).toEqual(['found']);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('skips number children during descent', () => {
|
|
590
|
+
const data = {
|
|
591
|
+
a: { key: 'found' },
|
|
592
|
+
num: 42,
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const result = r(data, '..key') as unknown[];
|
|
596
|
+
|
|
597
|
+
expect(result).toEqual(['found']);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('skips string children during descent', () => {
|
|
601
|
+
const data = {
|
|
602
|
+
a: { key: 'found' },
|
|
603
|
+
str: 'text',
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const result = r(data, '..key') as unknown[];
|
|
607
|
+
|
|
608
|
+
expect(result).toEqual(['found']);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('descends into nested objects checking both typeof and !== null', () => {
|
|
612
|
+
const data = {
|
|
613
|
+
a: { nested: { target: 'deep' } },
|
|
614
|
+
b: null,
|
|
615
|
+
c: 'text',
|
|
616
|
+
d: { target: 'shallow' },
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const result = r(data, '..target') as unknown[];
|
|
620
|
+
|
|
621
|
+
expect(result).toContain('deep');
|
|
622
|
+
expect(result).toContain('shallow');
|
|
623
|
+
expect(result).toHaveLength(2);
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
});
|