@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.
Files changed (179) hide show
  1. package/.gitattributes +1 -1
  2. package/CHANGELOG.md +10 -5
  3. package/LICENSE +1 -1
  4. package/README.md +56 -14
  5. package/dist/accessors/abstract-accessor.d.ts +22 -10
  6. package/dist/accessors/abstract-accessor.js +21 -8
  7. package/dist/accessors/abstract-integration-accessor.d.ts +22 -0
  8. package/dist/accessors/abstract-integration-accessor.js +23 -0
  9. package/dist/accessors/formats/any-accessor.d.ts +10 -8
  10. package/dist/accessors/formats/any-accessor.js +9 -8
  11. package/dist/accessors/formats/array-accessor.d.ts +2 -0
  12. package/dist/accessors/formats/array-accessor.js +2 -0
  13. package/dist/accessors/formats/env-accessor.d.ts +2 -0
  14. package/dist/accessors/formats/env-accessor.js +2 -0
  15. package/dist/accessors/formats/ini-accessor.d.ts +2 -0
  16. package/dist/accessors/formats/ini-accessor.js +2 -0
  17. package/dist/accessors/formats/json-accessor.d.ts +2 -0
  18. package/dist/accessors/formats/json-accessor.js +2 -0
  19. package/dist/accessors/formats/ndjson-accessor.d.ts +2 -0
  20. package/dist/accessors/formats/ndjson-accessor.js +2 -0
  21. package/dist/accessors/formats/object-accessor.d.ts +2 -0
  22. package/dist/accessors/formats/object-accessor.js +2 -0
  23. package/dist/accessors/formats/xml-accessor.d.ts +2 -0
  24. package/dist/accessors/formats/xml-accessor.js +2 -0
  25. package/dist/accessors/formats/yaml-accessor.d.ts +3 -1
  26. package/dist/accessors/formats/yaml-accessor.js +4 -2
  27. package/dist/cache/simple-path-cache.d.ts +51 -0
  28. package/dist/cache/simple-path-cache.js +72 -0
  29. package/dist/contracts/accessors-interface.d.ts +2 -0
  30. package/dist/contracts/factory-accessors-interface.d.ts +2 -0
  31. package/dist/contracts/filter-evaluator-interface.d.ts +28 -0
  32. package/dist/contracts/filter-evaluator-interface.js +1 -0
  33. package/dist/contracts/parse-integration-interface.d.ts +2 -0
  34. package/dist/contracts/parser-interface.d.ts +92 -0
  35. package/dist/contracts/parser-interface.js +1 -0
  36. package/dist/contracts/path-cache-interface.d.ts +7 -6
  37. package/dist/contracts/readable-accessors-interface.d.ts +11 -6
  38. package/dist/contracts/security-guard-interface.d.ts +2 -0
  39. package/dist/contracts/security-parser-interface.d.ts +2 -0
  40. package/dist/contracts/validatable-parser-interface.d.ts +59 -0
  41. package/dist/contracts/validatable-parser-interface.js +1 -0
  42. package/dist/contracts/writable-accessors-interface.d.ts +5 -0
  43. package/dist/core/accessor-factory.d.ts +124 -0
  44. package/dist/core/accessor-factory.js +157 -0
  45. package/dist/core/dot-notation-parser.d.ts +34 -5
  46. package/dist/core/dot-notation-parser.js +51 -10
  47. package/dist/core/inline-builder-accessor.d.ts +82 -0
  48. package/dist/core/inline-builder-accessor.js +107 -0
  49. package/dist/exceptions/accessor-exception.d.ts +9 -0
  50. package/dist/exceptions/accessor-exception.js +9 -0
  51. package/dist/exceptions/invalid-format-exception.d.ts +5 -0
  52. package/dist/exceptions/invalid-format-exception.js +5 -0
  53. package/dist/exceptions/parser-exception.d.ts +4 -0
  54. package/dist/exceptions/parser-exception.js +4 -0
  55. package/dist/exceptions/path-not-found-exception.d.ts +4 -0
  56. package/dist/exceptions/path-not-found-exception.js +4 -0
  57. package/dist/exceptions/readonly-violation-exception.d.ts +4 -0
  58. package/dist/exceptions/readonly-violation-exception.js +4 -0
  59. package/dist/exceptions/security-exception.d.ts +6 -0
  60. package/dist/exceptions/security-exception.js +6 -0
  61. package/dist/exceptions/unsupported-type-exception.d.ts +4 -0
  62. package/dist/exceptions/unsupported-type-exception.js +4 -0
  63. package/dist/exceptions/yaml-parse-exception.d.ts +4 -0
  64. package/dist/exceptions/yaml-parse-exception.js +4 -0
  65. package/dist/index.js +2 -1
  66. package/dist/inline.d.ts +22 -56
  67. package/dist/inline.js +39 -111
  68. package/dist/parser/xml-parser.js +23 -10
  69. package/dist/parser/yaml-parser.d.ts +54 -7
  70. package/dist/parser/yaml-parser.js +268 -51
  71. package/dist/path-query/segment-filter-parser.d.ts +142 -0
  72. package/dist/path-query/segment-filter-parser.js +384 -0
  73. package/dist/path-query/segment-parser.d.ts +98 -0
  74. package/dist/path-query/segment-parser.js +283 -0
  75. package/dist/path-query/segment-path-resolver.d.ts +149 -0
  76. package/dist/path-query/segment-path-resolver.js +351 -0
  77. package/dist/path-query/segment-type.d.ts +85 -0
  78. package/dist/path-query/segment-type.js +35 -0
  79. package/dist/security/forbidden-keys.d.ts +2 -2
  80. package/dist/security/forbidden-keys.js +5 -5
  81. package/dist/security/security-guard.d.ts +3 -1
  82. package/dist/security/security-guard.js +5 -2
  83. package/dist/security/security-parser.d.ts +10 -1
  84. package/dist/security/security-parser.js +10 -1
  85. package/dist/type-format.d.ts +2 -0
  86. package/dist/type-format.js +2 -0
  87. package/package.json +11 -3
  88. package/src/accessors/abstract-accessor.ts +23 -19
  89. package/src/accessors/abstract-integration-accessor.ts +27 -0
  90. package/src/accessors/formats/any-accessor.ts +11 -11
  91. package/src/accessors/formats/array-accessor.ts +2 -0
  92. package/src/accessors/formats/env-accessor.ts +2 -0
  93. package/src/accessors/formats/ini-accessor.ts +2 -0
  94. package/src/accessors/formats/json-accessor.ts +2 -0
  95. package/src/accessors/formats/ndjson-accessor.ts +2 -0
  96. package/src/accessors/formats/object-accessor.ts +2 -0
  97. package/src/accessors/formats/xml-accessor.ts +2 -0
  98. package/src/accessors/formats/yaml-accessor.ts +4 -2
  99. package/src/cache/simple-path-cache.ts +77 -0
  100. package/src/contracts/accessors-interface.ts +2 -0
  101. package/src/contracts/factory-accessors-interface.ts +2 -0
  102. package/src/contracts/filter-evaluator-interface.ts +30 -0
  103. package/src/contracts/parse-integration-interface.ts +2 -0
  104. package/src/contracts/parser-interface.ts +114 -0
  105. package/src/contracts/path-cache-interface.ts +8 -6
  106. package/src/contracts/readable-accessors-interface.ts +11 -6
  107. package/src/contracts/security-guard-interface.ts +2 -0
  108. package/src/contracts/security-parser-interface.ts +2 -0
  109. package/src/contracts/validatable-parser-interface.ts +64 -0
  110. package/src/contracts/writable-accessors-interface.ts +5 -0
  111. package/src/core/accessor-factory.ts +173 -0
  112. package/src/core/dot-notation-parser.ts +74 -11
  113. package/src/core/inline-builder-accessor.ts +163 -0
  114. package/src/exceptions/accessor-exception.ts +9 -0
  115. package/src/exceptions/invalid-format-exception.ts +5 -0
  116. package/src/exceptions/parser-exception.ts +4 -0
  117. package/src/exceptions/path-not-found-exception.ts +4 -0
  118. package/src/exceptions/readonly-violation-exception.ts +4 -0
  119. package/src/exceptions/security-exception.ts +6 -0
  120. package/src/exceptions/unsupported-type-exception.ts +4 -0
  121. package/src/exceptions/yaml-parse-exception.ts +4 -0
  122. package/src/index.ts +3 -1
  123. package/src/inline.ts +42 -120
  124. package/src/parser/xml-parser.ts +31 -10
  125. package/src/parser/yaml-parser.ts +310 -45
  126. package/src/path-query/segment-filter-parser.ts +444 -0
  127. package/src/path-query/segment-parser.ts +321 -0
  128. package/src/path-query/segment-path-resolver.ts +521 -0
  129. package/src/path-query/segment-type.ts +82 -0
  130. package/src/security/forbidden-keys.ts +5 -5
  131. package/src/security/security-guard.ts +7 -2
  132. package/src/security/security-parser.ts +18 -3
  133. package/src/type-format.ts +2 -0
  134. package/stryker.config.json +8 -10
  135. package/tests/accessors/abstract-accessor.test.ts +217 -0
  136. package/tests/accessors/abstract-integration-accessor.test.ts +37 -0
  137. package/tests/accessors/formats/any-accessor.test.ts +57 -0
  138. package/tests/accessors/formats/array-accessor.test.ts +42 -0
  139. package/tests/accessors/formats/env-accessor.test.ts +103 -0
  140. package/tests/accessors/formats/ini-accessor.test.ts +186 -0
  141. package/tests/accessors/{json-accessor.test.ts → formats/json-accessor.test.ts} +6 -6
  142. package/tests/accessors/formats/ndjson-accessor.test.ts +49 -0
  143. package/tests/accessors/formats/object-accessor.test.ts +172 -0
  144. package/tests/accessors/formats/xml-accessor.test.ts +162 -0
  145. package/tests/accessors/formats/yaml-accessor.test.ts +36 -0
  146. package/tests/cache/simple-path-cache.test.ts +168 -0
  147. package/tests/core/accessor-factory.test.ts +157 -0
  148. package/tests/core/dot-notation-parser-edge-cases.test.ts +415 -0
  149. package/tests/core/dot-notation-parser.test.ts +0 -288
  150. package/tests/core/inline-builder-accessor.test.ts +114 -0
  151. package/tests/exceptions/accessor-exception.test.ts +28 -0
  152. package/tests/exceptions/invalid-format-exception.test.ts +31 -0
  153. package/tests/exceptions/path-not-found-exception.test.ts +33 -0
  154. package/tests/exceptions/readonly-violation-exception.test.ts +35 -0
  155. package/tests/exceptions/security-exception.test.ts +33 -0
  156. package/tests/exceptions/unsupported-type-exception.test.ts +33 -0
  157. package/tests/exceptions/yaml-parse-exception.test.ts +38 -0
  158. package/tests/mocks/fake-path-cache.ts +4 -3
  159. package/tests/parity-from.test.ts +118 -0
  160. package/tests/parity.test.ts +227 -10
  161. package/tests/parser/xml-parser-mutations.test.ts +579 -0
  162. package/tests/parser/xml-parser-scanner.test.ts +332 -0
  163. package/tests/parser/xml-parser.test.ts +10 -334
  164. package/tests/parser/yaml-parser-mutations.test.ts +750 -0
  165. package/tests/parser/yaml-parser.test.ts +844 -18
  166. package/tests/path-query/segment-filter-parser-mutations.test.ts +735 -0
  167. package/tests/path-query/segment-filter-parser.test.ts +1091 -0
  168. package/tests/path-query/segment-parser-mutations.test.ts +539 -0
  169. package/tests/path-query/segment-parser.test.ts +606 -0
  170. package/tests/path-query/segment-path-resolver-mutations.test.ts +626 -0
  171. package/tests/path-query/segment-path-resolver.test.ts +1009 -0
  172. package/tests/security/security-guard-advanced.test.ts +413 -0
  173. package/tests/security/security-guard-forbidden-keys.test.ts +87 -0
  174. package/tests/security/security-guard.test.ts +3 -484
  175. package/tests/security/security-parser.test.ts +18 -14
  176. package/vitest.config.ts +3 -3
  177. package/benchmarks/get.bench.ts +0 -26
  178. package/benchmarks/parse.bench.ts +0 -41
  179. package/tests/accessors/accessors.test.ts +0 -1017
@@ -0,0 +1,539 @@
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} mutation tests`, () => {
9
+ let parser: SegmentParser;
10
+
11
+ beforeEach(() => {
12
+ parser = new SegmentParser(new SegmentFilterParser(new SecurityGuard()));
13
+ });
14
+
15
+ describe('$ prefix handling', () => {
16
+ it('parses "$" alone as empty segments', () => {
17
+ const result = parser.parseSegments('$');
18
+
19
+ expect(result).toHaveLength(0);
20
+ });
21
+
22
+ it('parses "$.key" stripping $ and dot', () => {
23
+ const result = parser.parseSegments('$.key');
24
+
25
+ expect(result).toHaveLength(1);
26
+ expect(result[0]).toEqual({ type: SegmentType.Key, value: 'key' });
27
+ });
28
+
29
+ it('does not strip $ from middle of path', () => {
30
+ const result = parser.parseSegments('price$');
31
+
32
+ expect(result).toHaveLength(1);
33
+ expect(result[0]).toEqual({ type: SegmentType.Key, value: 'price$' });
34
+ });
35
+ });
36
+
37
+ describe('descent bracket parsing', () => {
38
+ it('parses descent with bracket correctly reading inner content', () => {
39
+ const result = parser.parseSegments('..[longkeyname]');
40
+
41
+ expect(result[0].type).toBe(SegmentType.Descent);
42
+ expect((result[0] as { key: string }).key).toBe('longkeyname');
43
+ });
44
+
45
+ it('parses descent bracket with single-quoted key', () => {
46
+ const result = parser.parseSegments("..['my key']");
47
+
48
+ expect(result[0].type).toBe(SegmentType.Descent);
49
+ expect((result[0] as { key: string }).key).toBe('my key');
50
+ });
51
+
52
+ it('parses descent bracket with double-quoted key', () => {
53
+ const result = parser.parseSegments('..[" key "]');
54
+
55
+ expect(result[0].type).toBe(SegmentType.Descent);
56
+ expect((result[0] as { key: string }).key).toBe(' key ');
57
+ });
58
+
59
+ it('parses DescentMulti with spaces around comma-separated quoted keys', () => {
60
+ const result = parser.parseSegments("..[ 'a' , 'b' ]");
61
+
62
+ expect(result[0].type).toBe(SegmentType.DescentMulti);
63
+ expect((result[0] as { keys: string[] }).keys).toEqual(['a', 'b']);
64
+ });
65
+
66
+ it('returns non-quoted bracket as plain descent key', () => {
67
+ const result = parser.parseSegments('..[nonquoted]');
68
+
69
+ expect(result[0].type).toBe(SegmentType.Descent);
70
+ expect((result[0] as { key: string }).key).toBe('nonquoted');
71
+ });
72
+ });
73
+
74
+ describe('descent escaped dot handling', () => {
75
+ it('escape at end of path in descent key', () => {
76
+ const result = parser.parseSegments('..key');
77
+
78
+ expect(result[0].type).toBe(SegmentType.Descent);
79
+ expect((result[0] as { key: string }).key).toBe('key');
80
+ });
81
+
82
+ it('descent stops at dot boundary', () => {
83
+ const result = parser.parseSegments('..key.child');
84
+
85
+ expect(result).toHaveLength(2);
86
+ expect(result[0].type).toBe(SegmentType.Descent);
87
+ expect((result[0] as { key: string }).key).toBe('key');
88
+ expect(result[1].type).toBe(SegmentType.Key);
89
+ expect((result[1] as { value: string }).value).toBe('child');
90
+ });
91
+
92
+ it('descent stops at bracket boundary', () => {
93
+ const result = parser.parseSegments('..key[0]');
94
+
95
+ expect(result).toHaveLength(2);
96
+ expect(result[0].type).toBe(SegmentType.Descent);
97
+ expect((result[0] as { key: string }).key).toBe('key');
98
+ expect(result[1].type).toBe(SegmentType.Key);
99
+ });
100
+ });
101
+
102
+ describe('filter depth tracking', () => {
103
+ it('handles nested brackets in filter expression', () => {
104
+ const result = parser.parseSegments('items[?items[0]==1]');
105
+
106
+ expect(result[1].type).toBe(SegmentType.Filter);
107
+ });
108
+
109
+ it('handles deeply nested brackets in filter expression', () => {
110
+ const result = parser.parseSegments('items[?a[b[c]]==1]');
111
+
112
+ expect(result[1].type).toBe(SegmentType.Filter);
113
+ });
114
+ });
115
+
116
+ describe('[?...] filter dispatching vs [ bracket', () => {
117
+ it('dispatches [? to filter parser and [ to bracket parser distinctly', () => {
118
+ const filterResult = parser.parseSegments('x[?a>1]');
119
+ const bracketResult = parser.parseSegments('x[0]');
120
+
121
+ expect(filterResult[1].type).toBe(SegmentType.Filter);
122
+ expect(bracketResult[1].type).toBe(SegmentType.Key);
123
+ });
124
+
125
+ it('does not confuse [ at end of string with filter', () => {
126
+ const result = parser.parseSegments('x[abc]');
127
+
128
+ expect(result[1].type).toBe(SegmentType.Key);
129
+ expect((result[1] as { value: string }).value).toBe('abc');
130
+ });
131
+ });
132
+
133
+ describe('projection parsing', () => {
134
+ it('returns null segment for dot followed by non-brace char', () => {
135
+ const result = parser.parseSegments('a.b');
136
+
137
+ expect(result).toHaveLength(2);
138
+ expect(result[0]).toEqual({ type: SegmentType.Key, value: 'a' });
139
+ expect(result[1]).toEqual({ type: SegmentType.Key, value: 'b' });
140
+ });
141
+
142
+ it('reads projection inner content up to closing brace', () => {
143
+ const result = parser.parseSegments('.{alpha, beta}');
144
+
145
+ expect(result[0].type).toBe(SegmentType.Projection);
146
+ const fields = (result[0] as { fields: Array<{ alias: string; source: string }> })
147
+ .fields;
148
+ expect(fields).toHaveLength(2);
149
+ expect(fields[0]).toEqual({ alias: 'alpha', source: 'alpha' });
150
+ expect(fields[1]).toEqual({ alias: 'beta', source: 'beta' });
151
+ });
152
+
153
+ it('filters out empty entries from trailing comma in projection', () => {
154
+ const result = parser.parseSegments('.{name,}');
155
+
156
+ expect(result[0].type).toBe(SegmentType.Projection);
157
+ const fields = (result[0] as { fields: Array<{ alias: string; source: string }> })
158
+ .fields;
159
+ expect(fields).toHaveLength(1);
160
+ expect(fields[0].alias).toBe('name');
161
+ });
162
+
163
+ it('trims spaces from projection alias', () => {
164
+ const result = parser.parseSegments('.{ alias : source }');
165
+
166
+ expect(result[0].type).toBe(SegmentType.Projection);
167
+ const fields = (result[0] as { fields: Array<{ alias: string; source: string }> })
168
+ .fields;
169
+ expect(fields[0].alias).toBe('alias');
170
+ expect(fields[0].source).toBe('source');
171
+ });
172
+
173
+ it('trims spaces from projection entry without alias', () => {
174
+ const result = parser.parseSegments('.{ field }');
175
+
176
+ expect(result[0].type).toBe(SegmentType.Projection);
177
+ const fields = (result[0] as { fields: Array<{ alias: string; source: string }> })
178
+ .fields;
179
+ expect(fields[0].alias).toBe('field');
180
+ expect(fields[0].source).toBe('field');
181
+ });
182
+ });
183
+
184
+ describe('bracket parsing edge cases', () => {
185
+ it('reads bracket inner content accurately for multi-char keys', () => {
186
+ const result = parser.parseSegments('[longkey]');
187
+
188
+ expect(result[0]).toEqual({ type: SegmentType.Key, value: 'longkey' });
189
+ });
190
+
191
+ it('trims spaces in multi-index parts', () => {
192
+ const result = parser.parseSegments('items[ 0 , 1 , 2 ]');
193
+
194
+ expect(result[1].type).toBe(SegmentType.MultiIndex);
195
+ expect((result[1] as { indices: number[] }).indices).toEqual([0, 1, 2]);
196
+ });
197
+
198
+ it('trims spaces in multi-key parts', () => {
199
+ const result = parser.parseSegments("items[ 'a' , 'b' ]");
200
+
201
+ expect(result[1].type).toBe(SegmentType.MultiKey);
202
+ expect((result[1] as { keys: string[] }).keys).toEqual(['a', 'b']);
203
+ });
204
+
205
+ it('rejects empty parts as non-numeric and falls through to Key', () => {
206
+ const result = parser.parseSegments('items[,]');
207
+
208
+ expect(result[1].type).toBe(SegmentType.Key);
209
+ expect((result[1] as { value: string }).value).toBe(',');
210
+ });
211
+
212
+ it('rejects whitespace-only num parts and falls through to Key', () => {
213
+ const result = parser.parseSegments('items[ , ]');
214
+
215
+ expect(result[1].type).toBe(SegmentType.Key);
216
+ });
217
+
218
+ it('strips bracket quotes matching same delimiter (single)', () => {
219
+ const result = parser.parseSegments("items['key']");
220
+
221
+ expect(result[1]).toEqual({ type: SegmentType.Key, value: 'key' });
222
+ });
223
+
224
+ it('strips bracket quotes matching same delimiter (double)', () => {
225
+ const result = parser.parseSegments('items["key"]');
226
+
227
+ expect(result[1]).toEqual({ type: SegmentType.Key, value: 'key' });
228
+ });
229
+
230
+ it('does not strip mismatched quotes', () => {
231
+ const result = parser.parseSegments('items[\'key"]');
232
+
233
+ expect(result[1]).toEqual({ type: SegmentType.Key, value: '\'key"' });
234
+ });
235
+
236
+ it('does not strip quotes with extra content before', () => {
237
+ const result = parser.parseSegments("items[x'key']");
238
+
239
+ expect(result[1]).toEqual({ type: SegmentType.Key, value: "x'key'" });
240
+ });
241
+
242
+ it('does not strip quotes with extra content after', () => {
243
+ const result = parser.parseSegments("items['key'x]");
244
+
245
+ expect(result[1]).toEqual({ type: SegmentType.Key, value: "'key'x" });
246
+ });
247
+ });
248
+
249
+ describe('slice parsing edge cases', () => {
250
+ it('parses slice with only start [:] → end is null', () => {
251
+ const result = parser.parseSegments('items[2:]');
252
+
253
+ expect(result[1].type).toBe(SegmentType.Slice);
254
+ const slice = result[1] as {
255
+ start: number | null;
256
+ end: number | null;
257
+ step: number | null;
258
+ };
259
+ expect(slice.start).toBe(2);
260
+ expect(slice.end).toBeNull();
261
+ expect(slice.step).toBeNull();
262
+ });
263
+
264
+ it('parses slice with two colons [::] → all null parts', () => {
265
+ const result = parser.parseSegments('items[::]');
266
+
267
+ expect(result[1].type).toBe(SegmentType.Slice);
268
+ const slice = result[1] as {
269
+ start: number | null;
270
+ end: number | null;
271
+ step: number | null;
272
+ };
273
+ expect(slice.start).toBeNull();
274
+ expect(slice.end).toBeNull();
275
+ expect(slice.step).toBeNull();
276
+ });
277
+
278
+ it('throws InvalidFormatException with non-empty message for zero step', () => {
279
+ try {
280
+ parser.parseSegments('items[0:5:0]');
281
+ expect.fail('Should have thrown');
282
+ } catch (e) {
283
+ expect(e).toBeInstanceOf(InvalidFormatException);
284
+ expect((e as Error).message).not.toBe('');
285
+ expect((e as Error).message).toContain('zero');
286
+ }
287
+ });
288
+ });
289
+
290
+ describe('parseKey escaped dot handling', () => {
291
+ it('handles escaped dot at end of key path', () => {
292
+ const result = parser.parseSegments('a\\.b');
293
+
294
+ expect(result).toHaveLength(1);
295
+ expect((result[0] as { value: string }).value).toBe('a.b');
296
+ });
297
+
298
+ it('handles multiple escaped dots', () => {
299
+ const result = parser.parseSegments('a\\.b\\.c');
300
+
301
+ expect(result).toHaveLength(1);
302
+ expect((result[0] as { value: string }).value).toBe('a.b.c');
303
+ });
304
+
305
+ it('backslash not followed by dot is preserved as-is', () => {
306
+ const result = parser.parseSegments('a\\x');
307
+
308
+ expect(result).toHaveLength(1);
309
+ expect((result[0] as { value: string }).value).toBe('a\\x');
310
+ });
311
+ });
312
+
313
+ describe('allQuoted helper', () => {
314
+ it('detects parts with only closing quote as not quoted', () => {
315
+ const result = parser.parseSegments("data[a','b']");
316
+
317
+ expect(result[1].type).toBe(SegmentType.Key);
318
+ });
319
+
320
+ it('detects parts with empty string as not quoted', () => {
321
+ const result = parser.parseSegments("data['','']");
322
+
323
+ expect(result[1].type).toBe(SegmentType.MultiKey);
324
+ expect((result[1] as { keys: string[] }).keys).toEqual(['', '']);
325
+ });
326
+
327
+ it('rejects mixed quoted and unquoted parts', () => {
328
+ const result = parser.parseSegments("data['a',b]");
329
+
330
+ expect(result[1].type).toBe(SegmentType.Key);
331
+ });
332
+
333
+ it('rejects start-quoted-only parts', () => {
334
+ const result = parser.parseSegments("data['a,'b']");
335
+
336
+ expect(result[1].type).toBe(SegmentType.Key);
337
+ });
338
+
339
+ it('handles double-quoted multi-key with spaces', () => {
340
+ const result = parser.parseSegments('data[ "a" , "b" ]');
341
+
342
+ expect(result[1].type).toBe(SegmentType.MultiKey);
343
+ expect((result[1] as { keys: string[] }).keys).toEqual(['a', 'b']);
344
+ });
345
+ });
346
+
347
+ describe('parseKeys edge cases', () => {
348
+ it('handles bracket with multi-char content', () => {
349
+ const result = parser.parseKeys('a[longkey]');
350
+
351
+ expect(result).toEqual(['a', 'longkey']);
352
+ });
353
+
354
+ it('handles escaped dot correctly in parseKeys', () => {
355
+ const result = parser.parseKeys('a\\.b.c');
356
+
357
+ expect(result).toEqual(['a.b', 'c']);
358
+ });
359
+
360
+ it('empty path returns single empty key', () => {
361
+ const result = parser.parseKeys('');
362
+
363
+ expect(result).toEqual(['']);
364
+ });
365
+ });
366
+
367
+ describe('$ prefix with dot skip', () => {
368
+ it('parses "$.{name}" as key not projection when dot is consumed by $', () => {
369
+ const result = parser.parseSegments('$.{name}');
370
+
371
+ expect(result).toHaveLength(1);
372
+ expect(result[0].type).toBe(SegmentType.Key);
373
+ });
374
+
375
+ it('parses "$.." as empty segments when dot is consumed by $', () => {
376
+ const result = parser.parseSegments('$..');
377
+
378
+ expect(result).toHaveLength(0);
379
+ });
380
+
381
+ it('parses "$." followed by bracket as bracket segment', () => {
382
+ const result = parser.parseSegments('$.[0]');
383
+
384
+ expect(result).toHaveLength(1);
385
+ expect(result[0].type).toBe(SegmentType.Key);
386
+ expect((result[0] as { value: string }).value).toBe('0');
387
+ });
388
+ });
389
+
390
+ describe('descent parsing - j < len boundary', () => {
391
+ it('correctly scans bracket content up to ] boundary in descent', () => {
392
+ const result = parser.parseSegments('..[abc]');
393
+
394
+ expect(result[0].type).toBe(SegmentType.Descent);
395
+ expect((result[0] as { key: string }).key).toBe('abc');
396
+ });
397
+
398
+ it('handles bracket content with no closing ] in descent', () => {
399
+ const result = parser.parseSegments('..[open');
400
+
401
+ expect(result[0].type).toBe(SegmentType.Descent);
402
+ expect((result[0] as { key: string }).key).toBe('open');
403
+ });
404
+ });
405
+
406
+ describe('filter depth tracking - j < len boundary', () => {
407
+ it('correctly scans filter content up to ] boundary', () => {
408
+ const result = parser.parseSegments('x[?a>1]');
409
+
410
+ expect(result[1].type).toBe(SegmentType.Filter);
411
+ });
412
+
413
+ it('handles filter with no closing ]', () => {
414
+ const result = parser.parseSegments('x[?a>1');
415
+
416
+ expect(result[1].type).toBe(SegmentType.Filter);
417
+ });
418
+ });
419
+
420
+ describe('bracket parsing - j < len boundary', () => {
421
+ it('correctly scans bracket content up to ] boundary', () => {
422
+ const result = parser.parseSegments('[longkey]');
423
+
424
+ expect(result[0]).toEqual({ type: SegmentType.Key, value: 'longkey' });
425
+ });
426
+
427
+ it('handles bracket with no closing ]', () => {
428
+ const result = parser.parseSegments('[open');
429
+
430
+ expect(result[0]).toEqual({ type: SegmentType.Key, value: 'open' });
431
+ });
432
+ });
433
+
434
+ describe('projection parsing - j < len boundary', () => {
435
+ it('correctly scans projection content up to } boundary', () => {
436
+ const result = parser.parseSegments('.{field}');
437
+
438
+ expect(result[0].type).toBe(SegmentType.Projection);
439
+ });
440
+
441
+ it('handles projection with no closing }', () => {
442
+ const result = parser.parseSegments('.{field');
443
+
444
+ expect(result[0].type).toBe(SegmentType.Projection);
445
+ });
446
+ });
447
+
448
+ describe('pos.i + 1 arithmetic in descent/bracket/filter scanners', () => {
449
+ it('advances past descent bracket correctly', () => {
450
+ const result = parser.parseSegments('..[key].next');
451
+
452
+ expect(result).toHaveLength(2);
453
+ expect(result[0].type).toBe(SegmentType.Descent);
454
+ expect(result[1].type).toBe(SegmentType.Key);
455
+ expect((result[1] as { value: string }).value).toBe('next');
456
+ });
457
+
458
+ it('advances past filter bracket correctly', () => {
459
+ const result = parser.parseSegments('x[?a>1].next');
460
+
461
+ expect(result).toHaveLength(3);
462
+ expect(result[0].type).toBe(SegmentType.Key);
463
+ expect(result[1].type).toBe(SegmentType.Filter);
464
+ expect(result[2].type).toBe(SegmentType.Key);
465
+ });
466
+
467
+ it('advances past regular bracket correctly', () => {
468
+ const result = parser.parseSegments('[key].next');
469
+
470
+ expect(result).toHaveLength(2);
471
+ expect(result[1].type).toBe(SegmentType.Key);
472
+ expect((result[1] as { value: string }).value).toBe('next');
473
+ });
474
+ });
475
+
476
+ describe('allQuoted - trim method', () => {
477
+ it('trims whitespace from quoted parts in comma-separated list', () => {
478
+ const result = parser.parseSegments("data[ 'a' , 'b' ]");
479
+
480
+ expect(result[1].type).toBe(SegmentType.MultiKey);
481
+ expect((result[1] as { keys: string[] }).keys).toEqual(['a', 'b']);
482
+ });
483
+
484
+ it('rejects parts with only one matching quote', () => {
485
+ const result = parser.parseSegments("data['a, b']");
486
+
487
+ expect(result[1].type).toBe(SegmentType.Key);
488
+ });
489
+ });
490
+
491
+ describe('slice - sliceParts.length boundary', () => {
492
+ it('handles single-part slice (just colon) as null:null slice', () => {
493
+ const result = parser.parseSegments('items[:]');
494
+
495
+ expect(result[1].type).toBe(SegmentType.Slice);
496
+ const s = result[1] as { start: number | null; end: number | null };
497
+ expect(s.start).toBeNull();
498
+ expect(s.end).toBeNull();
499
+ });
500
+
501
+ it('sliceParts.length > 1 is needed to read end part', () => {
502
+ const result = parser.parseSegments('items[2:]');
503
+
504
+ expect(result[1].type).toBe(SegmentType.Slice);
505
+ const s = result[1] as { start: number | null; end: number | null };
506
+ expect(s.start).toBe(2);
507
+ expect(s.end).toBeNull();
508
+ });
509
+ });
510
+
511
+ describe('parseKeys - regex escape in placeholder', () => {
512
+ it('escapes special regex characters in placeholder for parseKeys', () => {
513
+ const result = parser.parseKeys('a\\.b');
514
+
515
+ expect(result).toEqual(['a.b']);
516
+ });
517
+
518
+ it('replaces placeholder correctly in final keys', () => {
519
+ const result = parser.parseKeys('x\\.y.z');
520
+
521
+ expect(result).toEqual(['x.y', 'z']);
522
+ });
523
+ });
524
+
525
+ describe('allQuoted - startsWith check', () => {
526
+ it('checks both single and double quote for startsWith', () => {
527
+ const result = parser.parseSegments('data["x","y"]');
528
+
529
+ expect(result[1].type).toBe(SegmentType.MultiKey);
530
+ expect((result[1] as { keys: string[] }).keys).toEqual(['x', 'y']);
531
+ });
532
+
533
+ it('rejects unquoted parts in allQuoted check', () => {
534
+ const result = parser.parseSegments("data[a,'b']");
535
+
536
+ expect(result[1].type).toBe(SegmentType.Key);
537
+ });
538
+ });
539
+ });