@safeaccess/inline 0.1.1 → 0.1.2
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 +10 -5
- package/LICENSE +1 -1
- package/README.md +56 -14
- package/dist/accessors/abstract-accessor.d.ts +22 -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 +22 -56
- package/dist/inline.js +39 -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 +3 -1
- package/dist/security/security-guard.js +5 -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 +23 -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 +42 -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 +7 -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 +332 -0
- package/tests/parser/xml-parser.test.ts +10 -334
- 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 +3 -484
- 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,606 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from 'vitest';
|
|
2
|
+
import { SegmentParser } from '../../src/path-query/segment-parser.js';
|
|
3
|
+
import { SegmentFilterParser } from '../../src/path-query/segment-filter-parser.js';
|
|
4
|
+
import { SegmentType } from '../../src/path-query/segment-type.js';
|
|
5
|
+
import { SecurityGuard } from '../../src/security/security-guard.js';
|
|
6
|
+
import { InvalidFormatException } from '../../src/exceptions/invalid-format-exception.js';
|
|
7
|
+
|
|
8
|
+
describe(SegmentParser.name, () => {
|
|
9
|
+
let parser: SegmentParser;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
parser = new SegmentParser(new SegmentFilterParser(new SecurityGuard()));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe(`${SegmentParser.name} > parseSegments`, () => {
|
|
16
|
+
it('parses a simple key path', () => {
|
|
17
|
+
const result = parser.parseSegments('name');
|
|
18
|
+
|
|
19
|
+
expect(result).toHaveLength(1);
|
|
20
|
+
expect(result[0]).toEqual({ type: SegmentType.Key, value: 'name' });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns empty array for an empty path', () => {
|
|
24
|
+
const result = parser.parseSegments('');
|
|
25
|
+
|
|
26
|
+
expect(result).toHaveLength(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('strips a leading $ prefix', () => {
|
|
30
|
+
const result = parser.parseSegments('$.name');
|
|
31
|
+
|
|
32
|
+
expect(result).toHaveLength(1);
|
|
33
|
+
expect(result[0]).toEqual({ type: SegmentType.Key, value: 'name' });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('strips a leading $ prefix without a dot', () => {
|
|
37
|
+
const result = parser.parseSegments('$name');
|
|
38
|
+
|
|
39
|
+
expect(result).toHaveLength(1);
|
|
40
|
+
expect(result[0]).toEqual({ type: SegmentType.Key, value: 'name' });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('parses a two-level dot-notation path', () => {
|
|
44
|
+
const result = parser.parseSegments('user.name');
|
|
45
|
+
|
|
46
|
+
expect(result).toHaveLength(2);
|
|
47
|
+
expect(result[0]).toEqual({ type: SegmentType.Key, value: 'user' });
|
|
48
|
+
expect(result[1]).toEqual({ type: SegmentType.Key, value: 'name' });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('parses a three-level dot-notation path', () => {
|
|
52
|
+
const result = parser.parseSegments('user.address.city');
|
|
53
|
+
|
|
54
|
+
expect(result).toHaveLength(3);
|
|
55
|
+
expect(result[2]).toEqual({ type: SegmentType.Key, value: 'city' });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('parses a wildcard * segment', () => {
|
|
59
|
+
const result = parser.parseSegments('users.*');
|
|
60
|
+
|
|
61
|
+
expect(result[1]).toEqual({ type: SegmentType.Wildcard });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('parses a bracket wildcard [*] segment', () => {
|
|
65
|
+
const result = parser.parseSegments('users[*]');
|
|
66
|
+
|
|
67
|
+
expect(result[1]).toEqual({ type: SegmentType.Wildcard });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('parses a bracket numeric index [0]', () => {
|
|
71
|
+
const result = parser.parseSegments('items[0]');
|
|
72
|
+
|
|
73
|
+
expect(result[1]).toEqual({ type: SegmentType.Key, value: '0' });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('parses a bracket quoted string key', () => {
|
|
77
|
+
const result = parser.parseSegments("data['key-with-dash']");
|
|
78
|
+
|
|
79
|
+
expect(result[1]).toEqual({ type: SegmentType.Key, value: 'key-with-dash' });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('parses a multi-index segment [0,1,2]', () => {
|
|
83
|
+
const result = parser.parseSegments('items[0,1,2]');
|
|
84
|
+
|
|
85
|
+
expect(result[1].type).toBe(SegmentType.MultiIndex);
|
|
86
|
+
expect((result[1] as { indices: number[] }).indices).toEqual([0, 1, 2]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("parses a multi-key segment ['a','b']", () => {
|
|
90
|
+
const result = parser.parseSegments("data['a','b']");
|
|
91
|
+
|
|
92
|
+
expect(result[1].type).toBe(SegmentType.MultiKey);
|
|
93
|
+
expect((result[1] as { keys: string[] }).keys).toEqual(['a', 'b']);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('parses a slice segment [1:5]', () => {
|
|
97
|
+
const result = parser.parseSegments('items[1:5]');
|
|
98
|
+
|
|
99
|
+
expect(result[1].type).toBe(SegmentType.Slice);
|
|
100
|
+
const slice = result[1] as {
|
|
101
|
+
start: number | null;
|
|
102
|
+
end: number | null;
|
|
103
|
+
step: number | null;
|
|
104
|
+
};
|
|
105
|
+
expect(slice.start).toBe(1);
|
|
106
|
+
expect(slice.end).toBe(5);
|
|
107
|
+
expect(slice.step).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('parses a slice segment with a step [0:10:2]', () => {
|
|
111
|
+
const result = parser.parseSegments('items[0:10:2]');
|
|
112
|
+
|
|
113
|
+
expect(result[1].type).toBe(SegmentType.Slice);
|
|
114
|
+
expect((result[1] as { step: number }).step).toBe(2);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('parses a slice with open start [::2]', () => {
|
|
118
|
+
const result = parser.parseSegments('items[::2]');
|
|
119
|
+
|
|
120
|
+
expect(result[1].type).toBe(SegmentType.Slice);
|
|
121
|
+
const slice = result[1] as {
|
|
122
|
+
start: number | null;
|
|
123
|
+
end: number | null;
|
|
124
|
+
step: number | null;
|
|
125
|
+
};
|
|
126
|
+
expect(slice.start).toBeNull();
|
|
127
|
+
expect(slice.end).toBeNull();
|
|
128
|
+
expect(slice.step).toBe(2);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('throws InvalidFormatException when slice step is zero', () => {
|
|
132
|
+
expect(() => parser.parseSegments('items[0:5:0]')).toThrow(InvalidFormatException);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('parses a recursive descent segment ..key', () => {
|
|
136
|
+
const result = parser.parseSegments('data..name');
|
|
137
|
+
|
|
138
|
+
const descent = result.filter((s) => s.type === SegmentType.Descent);
|
|
139
|
+
expect(descent).not.toHaveLength(0);
|
|
140
|
+
expect((descent[0] as { key: string }).key).toBe('name');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("parses a recursive descent with bracket key ..['key']", () => {
|
|
144
|
+
const result = parser.parseSegments("data..['nested']");
|
|
145
|
+
|
|
146
|
+
const descent = result.filter((s) => s.type === SegmentType.Descent);
|
|
147
|
+
expect((descent[0] as { key: string }).key).toBe('nested');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("parses a DescentMulti segment ..['a','b']", () => {
|
|
151
|
+
const result = parser.parseSegments("data..['a','b']");
|
|
152
|
+
|
|
153
|
+
const dm = result.filter((s) => s.type === SegmentType.DescentMulti);
|
|
154
|
+
expect(dm).not.toHaveLength(0);
|
|
155
|
+
expect((dm[0] as { keys: string[] }).keys).toEqual(['a', 'b']);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('parses a filter segment [?condition]', () => {
|
|
159
|
+
const result = parser.parseSegments('users[?age>18]');
|
|
160
|
+
|
|
161
|
+
expect(result[1].type).toBe(SegmentType.Filter);
|
|
162
|
+
const filter = result[1] as { expression: { conditions: unknown[] } };
|
|
163
|
+
expect(filter.expression.conditions).not.toHaveLength(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('parses a projection segment .{name,age}', () => {
|
|
167
|
+
const result = parser.parseSegments('users.{name,age}');
|
|
168
|
+
|
|
169
|
+
const proj = result.filter((s) => s.type === SegmentType.Projection);
|
|
170
|
+
expect(proj).not.toHaveLength(0);
|
|
171
|
+
const fields = (proj[0] as { fields: Array<{ alias: string }> }).fields;
|
|
172
|
+
expect(fields).toHaveLength(2);
|
|
173
|
+
expect(fields[0].alias).toBe('name');
|
|
174
|
+
expect(fields[1].alias).toBe('age');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('parses a projection with an alias .{fullName: name}', () => {
|
|
178
|
+
const result = parser.parseSegments('users.{fullName: name}');
|
|
179
|
+
|
|
180
|
+
const proj = result.filter((s) => s.type === SegmentType.Projection);
|
|
181
|
+
const fields = (proj[0] as { fields: Array<{ alias: string; source: string }> }).fields;
|
|
182
|
+
expect(fields[0].alias).toBe('fullName');
|
|
183
|
+
expect(fields[0].source).toBe('name');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('parses a path with an escaped dot as a literal key', () => {
|
|
187
|
+
const result = parser.parseSegments('data.key\\.with\\.dots');
|
|
188
|
+
|
|
189
|
+
expect((result[1] as { value: string }).value).toBe('key.with.dots');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe(`${SegmentParser.name} > parseKeys`, () => {
|
|
194
|
+
it('splits a simple dot-notation path into keys', () => {
|
|
195
|
+
const result = parser.parseKeys('user.address.city');
|
|
196
|
+
|
|
197
|
+
expect(result).toEqual(['user', 'address', 'city']);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('converts bracket notation to dot-notation keys', () => {
|
|
201
|
+
const result = parser.parseKeys('a[0][1]');
|
|
202
|
+
|
|
203
|
+
expect(result).toEqual(['a', '0', '1']);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('preserves escaped dots as literal dots in keys', () => {
|
|
207
|
+
const result = parser.parseKeys('data.key\\.dot');
|
|
208
|
+
|
|
209
|
+
expect(result).toEqual(['data', 'key.dot']);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('returns a single key for a path without separators', () => {
|
|
213
|
+
const result = parser.parseKeys('name');
|
|
214
|
+
|
|
215
|
+
expect(result).toEqual(['name']);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe(`${SegmentParser.name} > parseSegments edge cases`, () => {
|
|
220
|
+
it('parses an unquoted bracket descent key as a plain Descent segment', () => {
|
|
221
|
+
const result = parser.parseSegments('..[0]');
|
|
222
|
+
|
|
223
|
+
expect(result[0].type).toBe(SegmentType.Descent);
|
|
224
|
+
expect((result[0] as { key: string }).key).toBe('0');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('includes an escaped dot as a literal dot in a descent key', () => {
|
|
228
|
+
const result = parser.parseSegments('..key\\.sub');
|
|
229
|
+
|
|
230
|
+
expect(result[0].type).toBe(SegmentType.Descent);
|
|
231
|
+
expect((result[0] as { key: string }).key).toBe('key.sub');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('tracks depth for a nested bracket inside a filter expression', () => {
|
|
235
|
+
const result = parser.parseSegments('[?(items[0] == 1)]');
|
|
236
|
+
|
|
237
|
+
expect(result[0].type).toBe(SegmentType.Filter);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('falls through to Key type for an unquoted non-numeric comma-separated bracket', () => {
|
|
241
|
+
const result = parser.parseSegments('data[a,b]');
|
|
242
|
+
|
|
243
|
+
expect(result[1].type).toBe(SegmentType.Key);
|
|
244
|
+
expect((result[1] as { value: string }).value).toBe('a,b');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('parses a bracket with double-quoted keys', () => {
|
|
248
|
+
const result = parser.parseSegments('data["key"]');
|
|
249
|
+
|
|
250
|
+
expect(result[1]).toEqual({ type: SegmentType.Key, value: 'key' });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('parses a multi-key with double-quoted strings', () => {
|
|
254
|
+
const result = parser.parseSegments('data["a","b"]');
|
|
255
|
+
|
|
256
|
+
expect(result[1].type).toBe(SegmentType.MultiKey);
|
|
257
|
+
expect((result[1] as { keys: string[] }).keys).toEqual(['a', 'b']);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe(`${SegmentParser.name} > mutation boundary tests`, () => {
|
|
262
|
+
it('treats a bare $ as empty segments (no dot follows)', () => {
|
|
263
|
+
const result = parser.parseSegments('$');
|
|
264
|
+
|
|
265
|
+
expect(result).toHaveLength(0);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('parses $ without a dot followed by a key segment', () => {
|
|
269
|
+
const result = parser.parseSegments('$name');
|
|
270
|
+
|
|
271
|
+
expect(result).toHaveLength(1);
|
|
272
|
+
expect(result[0]).toEqual({ type: SegmentType.Key, value: 'name' });
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('skips the dot after $ when followed by a path', () => {
|
|
276
|
+
const result = parser.parseSegments('$.a.b');
|
|
277
|
+
|
|
278
|
+
expect(result).toHaveLength(2);
|
|
279
|
+
expect(result[0]).toEqual({ type: SegmentType.Key, value: 'a' });
|
|
280
|
+
expect(result[1]).toEqual({ type: SegmentType.Key, value: 'b' });
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('does not skip the second char after $ when it is not a dot', () => {
|
|
284
|
+
const r1 = parser.parseSegments('$abc');
|
|
285
|
+
const r2 = parser.parseSegments('$.abc');
|
|
286
|
+
|
|
287
|
+
expect(r1).toEqual(r2);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('distinguishes a single dot from a double dot at path end', () => {
|
|
291
|
+
const result = parser.parseSegments('a.');
|
|
292
|
+
|
|
293
|
+
expect(result).toHaveLength(1);
|
|
294
|
+
expect(result[0]).toEqual({ type: SegmentType.Key, value: 'a' });
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('parses descent when double dot is at the end of path', () => {
|
|
298
|
+
const result = parser.parseSegments('a..');
|
|
299
|
+
|
|
300
|
+
expect(result).toHaveLength(2);
|
|
301
|
+
expect(result[0]).toEqual({ type: SegmentType.Key, value: 'a' });
|
|
302
|
+
expect(result[1]).toEqual({ type: SegmentType.Descent, key: '' });
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('parses [? at end of path without closing bracket', () => {
|
|
306
|
+
const result = parser.parseSegments('[?x>1]');
|
|
307
|
+
|
|
308
|
+
expect(result[0].type).toBe(SegmentType.Filter);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('parses a bare [ at the last character as a key segment', () => {
|
|
312
|
+
const result = parser.parseSegments('a[0]');
|
|
313
|
+
|
|
314
|
+
expect(result).toHaveLength(2);
|
|
315
|
+
expect(result[0]).toEqual({ type: SegmentType.Key, value: 'a' });
|
|
316
|
+
expect(result[1]).toEqual({ type: SegmentType.Key, value: '0' });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('handles a descent bracket where the bracket is at the exact pos.i boundary', () => {
|
|
320
|
+
const result = parser.parseSegments('..[key]');
|
|
321
|
+
|
|
322
|
+
expect(result[0].type).toBe(SegmentType.Descent);
|
|
323
|
+
expect((result[0] as { key: string }).key).toBe('key');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('handles a descent bracket where inner is empty', () => {
|
|
327
|
+
const result = parser.parseSegments('..[]');
|
|
328
|
+
|
|
329
|
+
expect(result[0].type).toBe(SegmentType.Descent);
|
|
330
|
+
expect((result[0] as { key: string }).key).toBe('');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('parses projection when { appears right after a dot', () => {
|
|
334
|
+
const result = parser.parseSegments('.{name,age}');
|
|
335
|
+
|
|
336
|
+
expect(result[0].type).toBe(SegmentType.Projection);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('returns null projection when pos is at end of string', () => {
|
|
340
|
+
const result = parser.parseSegments('key.');
|
|
341
|
+
|
|
342
|
+
expect(result).toHaveLength(1);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('skips non-quoted comma-separated parts where one trim result is empty', () => {
|
|
346
|
+
const result = parser.parseSegments('data[1, ,3]');
|
|
347
|
+
|
|
348
|
+
expect(result[1].type).toBe(SegmentType.Key);
|
|
349
|
+
expect((result[1] as { value: string }).value).toBe('1, ,3');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('detects that trimmed parts with spaces are not all numeric', () => {
|
|
353
|
+
const result = parser.parseSegments('data[ 1, 2 ]');
|
|
354
|
+
|
|
355
|
+
expect(result[1].type).toBe(SegmentType.MultiIndex);
|
|
356
|
+
expect((result[1] as { indices: number[] }).indices).toEqual([1, 2]);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('handles slice where end part is an empty string', () => {
|
|
360
|
+
const result = parser.parseSegments('items[1:]');
|
|
361
|
+
|
|
362
|
+
expect(result[1].type).toBe(SegmentType.Slice);
|
|
363
|
+
const slice = result[1] as {
|
|
364
|
+
start: number | null;
|
|
365
|
+
end: number | null;
|
|
366
|
+
step: number | null;
|
|
367
|
+
};
|
|
368
|
+
expect(slice.start).toBe(1);
|
|
369
|
+
expect(slice.end).toBeNull();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('handles slice with only start [3:]', () => {
|
|
373
|
+
const sliceResult = parser.parseSegments('items[3:]');
|
|
374
|
+
const slice = sliceResult[1] as {
|
|
375
|
+
start: number | null;
|
|
376
|
+
end: number | null;
|
|
377
|
+
step: number | null;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
expect(slice.start).toBe(3);
|
|
381
|
+
expect(slice.end).toBeNull();
|
|
382
|
+
expect(slice.step).toBeNull();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('correctly detects allQuoted with mixed single and double quotes', () => {
|
|
386
|
+
const result = parser.parseSegments('data[\'a\',"b"]');
|
|
387
|
+
|
|
388
|
+
expect(result[1].type).toBe(SegmentType.MultiKey);
|
|
389
|
+
expect((result[1] as { keys: string[] }).keys).toEqual(['a', 'b']);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('detects non-quoted parts in allQuoted check when a part starts with quote but not ends', () => {
|
|
393
|
+
const result = parser.parseSegments('data["abc]');
|
|
394
|
+
|
|
395
|
+
expect(result[1].type).toBe(SegmentType.Key);
|
|
396
|
+
expect((result[1] as { value: string }).value).toBe('"abc');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('returns false in allQuoted when a part starts with double-quote but ends without it', () => {
|
|
400
|
+
const result = parser.parseSegments('data["abc,def]');
|
|
401
|
+
|
|
402
|
+
expect(result[1].type).toBe(SegmentType.Key);
|
|
403
|
+
expect((result[1] as { value: string }).value).toBe('"abc,def');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('returns false in allQuoted for parts starting and ending with empty string', () => {
|
|
407
|
+
const result = parser.parseSegments('data[,]');
|
|
408
|
+
|
|
409
|
+
expect(result[1].type).toBe(SegmentType.Key);
|
|
410
|
+
expect((result[1] as { value: string }).value).toBe(',');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('handles escaped dot at the very end of a key path', () => {
|
|
414
|
+
const result = parser.parseSegments('key\\.');
|
|
415
|
+
|
|
416
|
+
expect(result[0]).toEqual({ type: SegmentType.Key, value: 'key.' });
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('handles escaped dot at the very end of a descent key', () => {
|
|
420
|
+
const result = parser.parseSegments('..key\\.');
|
|
421
|
+
|
|
422
|
+
expect(result[0]).toEqual({ type: SegmentType.Descent, key: 'key.' });
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('does not treat backslash as escape when not followed by dot', () => {
|
|
426
|
+
const result = parser.parseSegments('ke\\y');
|
|
427
|
+
|
|
428
|
+
expect(result[0]).toEqual({ type: SegmentType.Key, value: 'ke\\y' });
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('does not treat backslash as escape in descent when not followed by dot', () => {
|
|
432
|
+
const result = parser.parseSegments('..ke\\y');
|
|
433
|
+
|
|
434
|
+
expect(result[0]).toEqual({ type: SegmentType.Descent, key: 'ke\\y' });
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('handles backslash at the very last character of the path in parseKey', () => {
|
|
438
|
+
const result = parser.parseSegments('key\\');
|
|
439
|
+
|
|
440
|
+
expect(result[0]).toEqual({ type: SegmentType.Key, value: 'key\\' });
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('handles backslash at the very last character in a descent key', () => {
|
|
444
|
+
const result = parser.parseSegments('..key\\');
|
|
445
|
+
|
|
446
|
+
expect(result[0]).toEqual({ type: SegmentType.Descent, key: 'key\\' });
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('produces correct replacement in parseKeys for escaped dots in brackets', () => {
|
|
450
|
+
const result = parser.parseKeys('a\\.b');
|
|
451
|
+
|
|
452
|
+
expect(result).toEqual(['a.b']);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('handles parseKeys with no special characters', () => {
|
|
456
|
+
const result = parser.parseKeys('simple');
|
|
457
|
+
|
|
458
|
+
expect(result).toEqual(['simple']);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('handles parseKeys with a dot followed by a bracket', () => {
|
|
462
|
+
const result = parser.parseKeys('a.b[0]');
|
|
463
|
+
|
|
464
|
+
expect(result).toEqual(['a', 'b', '0']);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('resolves regex placeholder replacement correctly for special chars', () => {
|
|
468
|
+
const result = parser.parseKeys('data\\.key\\.more');
|
|
469
|
+
|
|
470
|
+
expect(result).toEqual(['data.key.more']);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('handles multikey in descent with spaces around quoted parts', () => {
|
|
474
|
+
const result = parser.parseSegments("..[ 'a' , 'b' ]");
|
|
475
|
+
|
|
476
|
+
expect(result[0].type).toBe(SegmentType.DescentMulti);
|
|
477
|
+
expect((result[0] as { keys: string[] }).keys).toEqual(['a', 'b']);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('handles projection when pos.i is past len (empty projection)', () => {
|
|
481
|
+
const result = parser.parseSegments('.{}');
|
|
482
|
+
|
|
483
|
+
expect(result[0].type).toBe(SegmentType.Projection);
|
|
484
|
+
const proj = result[0] as { fields: Array<{ alias: string; source: string }> };
|
|
485
|
+
expect(proj.fields).toHaveLength(0);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('handles bracket parse when inner string has no closing bracket', () => {
|
|
489
|
+
const result = parser.parseSegments('[abc');
|
|
490
|
+
|
|
491
|
+
expect(result[0].type).toBe(SegmentType.Key);
|
|
492
|
+
expect((result[0] as { value: string }).value).toBe('abc');
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('correctly distinguishes multi-index with spaces from non-numeric', () => {
|
|
496
|
+
const r1 = parser.parseSegments('data[ 0 , 1 ]');
|
|
497
|
+
expect(r1[1].type).toBe(SegmentType.MultiIndex);
|
|
498
|
+
expect((r1[1] as { indices: number[] }).indices).toEqual([0, 1]);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('parses filter where the bracket depth needs tracking with nested brackets', () => {
|
|
502
|
+
const result = parser.parseSegments('[?items[0] == 1 && items[1] == 2]');
|
|
503
|
+
|
|
504
|
+
expect(result[0].type).toBe(SegmentType.Filter);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('treats first bracket-close correctly in parseFilter depth tracking', () => {
|
|
508
|
+
const result = parser.parseSegments('[?a[0]>1]');
|
|
509
|
+
|
|
510
|
+
expect(result[0].type).toBe(SegmentType.Filter);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('returns false for allQuoted when a part has empty string content', () => {
|
|
514
|
+
const result = parser.parseSegments("data['',1]");
|
|
515
|
+
|
|
516
|
+
expect(result[1].type).toBe(SegmentType.Key);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('checks allQuoted with double-quoted parts that do not endsWith double-quote', () => {
|
|
520
|
+
const result = parser.parseSegments('data["x","y"]');
|
|
521
|
+
|
|
522
|
+
expect(result[1].type).toBe(SegmentType.MultiKey);
|
|
523
|
+
expect((result[1] as { keys: string[] }).keys).toEqual(['x', 'y']);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('check allQuoted returns false when part starts with empty prefix test', () => {
|
|
527
|
+
const result = parser.parseSegments('data[ab,cd]');
|
|
528
|
+
|
|
529
|
+
expect(result[1].type).toBe(SegmentType.Key);
|
|
530
|
+
expect((result[1] as { value: string }).value).toBe('ab,cd');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('allQuoted rejects parts that start with single quote but do not end with single quote', () => {
|
|
534
|
+
const result = parser.parseSegments("data[b','a']");
|
|
535
|
+
|
|
536
|
+
expect(result[1].type).toBe(SegmentType.Key);
|
|
537
|
+
expect((result[1] as { value: string }).value).toBe("b','a'");
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('allQuoted rejects parts that end with single quote but do not start with single quote', () => {
|
|
541
|
+
const result = parser.parseSegments("data[abc','def']");
|
|
542
|
+
|
|
543
|
+
expect(result[1].type).toBe(SegmentType.Key);
|
|
544
|
+
expect((result[1] as { value: string }).value).toBe("abc','def'");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('allQuoted rejects parts that start with double quote but do not end with double quote', () => {
|
|
548
|
+
const result = parser.parseSegments('data[b","a"]');
|
|
549
|
+
|
|
550
|
+
expect(result[1].type).toBe(SegmentType.Key);
|
|
551
|
+
expect((result[1] as { value: string }).value).toBe('b","a"');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('allQuoted uses endsWith for double-quoted second check not startsWith', () => {
|
|
555
|
+
const result = parser.parseSegments('data["a","b"]');
|
|
556
|
+
|
|
557
|
+
expect(result[1].type).toBe(SegmentType.MultiKey);
|
|
558
|
+
expect((result[1] as { keys: string[] }).keys).toEqual(['a', 'b']);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('allQuoted identifies properly quoted multi-key in descent brackets', () => {
|
|
562
|
+
const result = parser.parseSegments("..['x','y']");
|
|
563
|
+
|
|
564
|
+
expect(result[0].type).toBe(SegmentType.DescentMulti);
|
|
565
|
+
expect((result[0] as { keys: string[] }).keys).toEqual(['x', 'y']);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('quotedMatch regex requires ^ anchor (rejects text before quote)', () => {
|
|
569
|
+
const result = parser.parseSegments("..[x'key']");
|
|
570
|
+
|
|
571
|
+
expect(result[0].type).toBe(SegmentType.Descent);
|
|
572
|
+
expect((result[0] as { key: string }).key).toBe("x'key'");
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('quotedMatch regex requires $ anchor (rejects text after quote)', () => {
|
|
576
|
+
const result = parser.parseSegments("..['key'x]");
|
|
577
|
+
|
|
578
|
+
expect(result[0].type).toBe(SegmentType.Descent);
|
|
579
|
+
expect((result[0] as { key: string }).key).toBe("'key'x");
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('quotedMatch in parseBracket requires ^ anchor', () => {
|
|
583
|
+
const result = parser.parseSegments("[x'key']");
|
|
584
|
+
|
|
585
|
+
expect(result[0]).toEqual({ type: SegmentType.Key, value: "x'key'" });
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('quotedMatch in parseBracket requires $ anchor', () => {
|
|
589
|
+
const result = parser.parseSegments("['key'x]");
|
|
590
|
+
|
|
591
|
+
expect(result[0]).toEqual({ type: SegmentType.Key, value: "'key'x" });
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('parseKeys placeholder regex escapes special characters correctly', () => {
|
|
595
|
+
const result = parser.parseKeys('a\\.b.c');
|
|
596
|
+
|
|
597
|
+
expect(result).toEqual(['a.b', 'c']);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('parseKeys escape regex replacement uses backslash-escaped char not empty string', () => {
|
|
601
|
+
const result = parser.parseKeys('x\\.y\\.z');
|
|
602
|
+
|
|
603
|
+
expect(result).toEqual(['x.y.z']);
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
});
|