@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,735 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { SegmentFilterParser } from '../../src/path-query/segment-filter-parser.js';
3
+ import { SecurityGuard } from '../../src/security/security-guard.js';
4
+ import { InvalidFormatException } from '../../src/exceptions/invalid-format-exception.js';
5
+
6
+ describe(`${SegmentFilterParser.name} > parse edge cases`, () => {
7
+ it('trims whitespace from tokenized conditions', () => {
8
+ const parser = new SegmentFilterParser(new SecurityGuard());
9
+
10
+ const expr = parser.parse(' age > 18 ');
11
+
12
+ expect(expr.conditions[0].field).toBe('age');
13
+ expect(expr.conditions[0].value).toBe(18);
14
+ });
15
+
16
+ it('handles double-quoted logical expression with && inside quotes', () => {
17
+ const parser = new SegmentFilterParser(new SecurityGuard());
18
+
19
+ const expr = parser.parse('name=="a && b"');
20
+
21
+ expect(expr.conditions).toHaveLength(1);
22
+ expect(expr.conditions[0].value).toBe('a && b');
23
+ });
24
+
25
+ it('handles double-quoted logical expression with || inside quotes', () => {
26
+ const parser = new SegmentFilterParser(new SecurityGuard());
27
+
28
+ const expr = parser.parse('name=="a || b"');
29
+
30
+ expect(expr.conditions).toHaveLength(1);
31
+ expect(expr.conditions[0].value).toBe('a || b');
32
+ });
33
+
34
+ it('splits && operator correctly at boundary positions', () => {
35
+ const parser = new SegmentFilterParser(new SecurityGuard());
36
+
37
+ const expr = parser.parse('a>1 && b>2');
38
+
39
+ expect(expr.logicals).toEqual(['&&']);
40
+ expect(expr.conditions).toHaveLength(2);
41
+ expect(expr.conditions[0].field).toBe('a');
42
+ expect(expr.conditions[1].field).toBe('b');
43
+ });
44
+
45
+ it('splits || operator correctly at boundary positions', () => {
46
+ const parser = new SegmentFilterParser(new SecurityGuard());
47
+
48
+ const expr = parser.parse('a>1 || b>2');
49
+
50
+ expect(expr.logicals).toEqual(['||']);
51
+ expect(expr.conditions).toHaveLength(2);
52
+ });
53
+
54
+ it('trims whitespace from function comparison value', () => {
55
+ const parser = new SegmentFilterParser(new SecurityGuard());
56
+
57
+ const expr = parser.parse("starts_with(@.name, 'Al') > 0 ");
58
+
59
+ expect(expr.conditions[0].value).toBe(0);
60
+ });
61
+
62
+ it('trims whitespace from function arguments', () => {
63
+ const parser = new SegmentFilterParser(new SecurityGuard());
64
+
65
+ const expr = parser.parse("starts_with( @.name , 'Al' )");
66
+
67
+ expect(expr.conditions[0].funcArgs).toBeDefined();
68
+ expect(expr.conditions[0].funcArgs![0]).toBe('@.name');
69
+ expect(expr.conditions[0].funcArgs![1]).toBe("'Al'");
70
+ });
71
+
72
+ it('trims whitespace from boolean function arguments', () => {
73
+ const parser = new SegmentFilterParser(new SecurityGuard());
74
+
75
+ const expr = parser.parse('values( @.items )');
76
+
77
+ expect(expr.conditions[0].funcArgs).toBeDefined();
78
+ expect(expr.conditions[0].funcArgs![0]).toBe('@.items');
79
+ });
80
+
81
+ it('rejects mismatched single-double quote delimiters as unquoted value', () => {
82
+ const parser = new SegmentFilterParser(new SecurityGuard());
83
+
84
+ const expr = parser.parse('name == \'Alice"');
85
+
86
+ expect(expr.conditions[0].value).toBe('\'Alice"');
87
+ });
88
+
89
+ it('rejects string starting with quote but not ending with same quote', () => {
90
+ const parser = new SegmentFilterParser(new SecurityGuard());
91
+
92
+ const expr = parser.parse("name == 'incomplete");
93
+
94
+ expect(expr.conditions[0].value).toBe("'incomplete");
95
+ });
96
+
97
+ it('rejects string ending with quote but not starting with it', () => {
98
+ const parser = new SegmentFilterParser(new SecurityGuard());
99
+
100
+ const expr = parser.parse("name == incomplete'");
101
+
102
+ expect(expr.conditions[0].value).toBe("incomplete'");
103
+ });
104
+
105
+ it('rejects string starting with double quote but ending without it', () => {
106
+ const parser = new SegmentFilterParser(new SecurityGuard());
107
+
108
+ const expr = parser.parse('name == "incomplete');
109
+
110
+ expect(expr.conditions[0].value).toBe('"incomplete');
111
+ });
112
+
113
+ it('rejects string ending with double quote but not starting with it', () => {
114
+ const parser = new SegmentFilterParser(new SecurityGuard());
115
+
116
+ const expr = parser.parse('name == incomplete"');
117
+
118
+ expect(expr.conditions[0].value).toBe('incomplete"');
119
+ });
120
+
121
+ it('parses integer value without decimal as int (not float)', () => {
122
+ const parser = new SegmentFilterParser(new SecurityGuard());
123
+
124
+ const expr = parser.parse('age > 25');
125
+
126
+ expect(expr.conditions[0].value).toBe(25);
127
+ expect(Number.isInteger(expr.conditions[0].value)).toBe(true);
128
+ });
129
+
130
+ it('parses float value with decimal as float', () => {
131
+ const parser = new SegmentFilterParser(new SecurityGuard());
132
+
133
+ const expr = parser.parse('price > 9.99');
134
+
135
+ expect(expr.conditions[0].value).toBe(9.99);
136
+ expect(Number.isInteger(expr.conditions[0].value)).toBe(false);
137
+ });
138
+
139
+ it('throws InvalidFormatException with message containing the invalid token', () => {
140
+ const parser = new SegmentFilterParser(new SecurityGuard());
141
+
142
+ expect(() => parser.parse('no-operator-here')).toThrow(InvalidFormatException);
143
+
144
+ try {
145
+ parser.parse('bad-token');
146
+ } catch (e) {
147
+ expect((e as Error).message).toContain('bad-token');
148
+ expect((e as Error).message).not.toBe('');
149
+ }
150
+ });
151
+
152
+ it('parses function-compare with multi-char value correctly', () => {
153
+ const parser = new SegmentFilterParser(new SecurityGuard());
154
+
155
+ const expr = parser.parse("starts_with(@.name, 'Alice') == true");
156
+
157
+ expect(expr.conditions[0].value).toBe(true);
158
+ expect(expr.conditions[0].func).toBe('starts_with');
159
+ });
160
+ });
161
+
162
+ describe(`${SegmentFilterParser.name} > evaluate edge cases`, () => {
163
+ it('evaluates arithmetic subtraction correctly (not addition)', () => {
164
+ const parser = new SegmentFilterParser(new SecurityGuard());
165
+
166
+ const expr = parser.parse('@.a - @.b > 0');
167
+
168
+ expect(parser.evaluate({ a: 10, b: 3 }, expr)).toBe(true);
169
+ expect(parser.evaluate({ a: 3, b: 10 }, expr)).toBe(false);
170
+ });
171
+
172
+ it('evaluates arithmetic division correctly (not multiplication)', () => {
173
+ const parser = new SegmentFilterParser(new SecurityGuard());
174
+
175
+ const expr = parser.parse('@.a / @.b > 2');
176
+
177
+ expect(parser.evaluate({ a: 10, b: 3 }, expr)).toBe(true);
178
+ expect(parser.evaluate({ a: 2, b: 3 }, expr)).toBe(false);
179
+ });
180
+
181
+ it('detects arithmetic expression without spaces around operator', () => {
182
+ const parser = new SegmentFilterParser(new SecurityGuard());
183
+
184
+ const expr = parser.parse('@.a+@.b > 0');
185
+
186
+ expect(parser.evaluate({ a: 1, b: 2 }, expr)).toBe(true);
187
+ });
188
+
189
+ it('detects arithmetic expression with extra spaces around operator', () => {
190
+ const parser = new SegmentFilterParser(new SecurityGuard());
191
+
192
+ const expr = parser.parse('@.a + @.b > 0');
193
+
194
+ expect(parser.evaluate({ a: 1, b: 2 }, expr)).toBe(true);
195
+ });
196
+
197
+ it('returns null from arithmetic when left operand is missing', () => {
198
+ const parser = new SegmentFilterParser(new SecurityGuard());
199
+
200
+ const expr = parser.parse('@.missing + @.b > 0');
201
+
202
+ expect(parser.evaluate({ b: 5 }, expr)).toBe(false);
203
+ });
204
+
205
+ it('returns null from arithmetic when right operand is missing', () => {
206
+ const parser = new SegmentFilterParser(new SecurityGuard());
207
+
208
+ const expr = parser.parse('@.a + @.missing > 0');
209
+
210
+ expect(parser.evaluate({ a: 5 }, expr)).toBe(false);
211
+ });
212
+
213
+ it('evaluates starts_with with non-string returns false', () => {
214
+ const parser = new SegmentFilterParser(new SecurityGuard());
215
+
216
+ const expr = parser.parse("starts_with(@.score, 'A')");
217
+
218
+ expect(parser.evaluate({ score: 42 }, expr)).toBe(false);
219
+ });
220
+
221
+ it('evaluates contains on array with exact string match', () => {
222
+ const parser = new SegmentFilterParser(new SecurityGuard());
223
+
224
+ const expr = parser.parse("contains(@.tags, 'js')");
225
+
226
+ expect(parser.evaluate({ tags: ['js', 'ts'] }, expr)).toBe(true);
227
+ expect(parser.evaluate({ tags: ['python'] }, expr)).toBe(false);
228
+ });
229
+
230
+ it('evaluates contains on string with substring', () => {
231
+ const parser = new SegmentFilterParser(new SecurityGuard());
232
+
233
+ const expr = parser.parse("contains(@.name, 'li')");
234
+
235
+ expect(parser.evaluate({ name: 'Alice' }, expr)).toBe(true);
236
+ expect(parser.evaluate({ name: 'Bob' }, expr)).toBe(false);
237
+ });
238
+
239
+ it('evaluates values returns count of array', () => {
240
+ const parser = new SegmentFilterParser(new SecurityGuard());
241
+
242
+ const expr = parser.parse('values(@.items) > 2');
243
+
244
+ expect(parser.evaluate({ items: [1, 2, 3] }, expr)).toBe(true);
245
+ expect(parser.evaluate({ items: [1] }, expr)).toBe(false);
246
+ });
247
+
248
+ it('throws InvalidFormatException for unknown function with descriptive message', () => {
249
+ const parser = new SegmentFilterParser(new SecurityGuard());
250
+ const expr = parser.parse('unknown_fn(@.x) > 0');
251
+
252
+ try {
253
+ parser.evaluate({ x: 1 }, expr);
254
+ expect.fail('Should have thrown');
255
+ } catch (e) {
256
+ expect(e).toBeInstanceOf(InvalidFormatException);
257
+ expect((e as Error).message).toContain('unknown_fn');
258
+ expect((e as Error).message).not.toBe('');
259
+ }
260
+ });
261
+
262
+ it('resolves dot-separated field in non-arithmetic condition', () => {
263
+ const parser = new SegmentFilterParser(new SecurityGuard());
264
+
265
+ const expr = parser.parse("user.name == 'Alice'");
266
+
267
+ expect(parser.evaluate({ user: { name: 'Alice' } }, expr)).toBe(true);
268
+ expect(parser.evaluate({ user: { name: 'Bob' } }, expr)).toBe(false);
269
+ });
270
+
271
+ it('returns null for missing nested field in dot-separated path', () => {
272
+ const parser = new SegmentFilterParser(new SecurityGuard());
273
+
274
+ const expr = parser.parse("user.profile.name == 'Alice'");
275
+
276
+ expect(parser.evaluate({ user: {} }, expr)).toBe(false);
277
+ });
278
+
279
+ it('resolves arithmetic with multi-part field names', () => {
280
+ const parser = new SegmentFilterParser(new SecurityGuard());
281
+
282
+ const expr = parser.parse('@.price * @.qty > 100');
283
+
284
+ expect(parser.evaluate({ price: 50, qty: 3 }, expr)).toBe(true);
285
+ expect(parser.evaluate({ price: 10, qty: 5 }, expr)).toBe(false);
286
+ });
287
+
288
+ it('evaluates funcArgs undefined fallback with correct default', () => {
289
+ const parser = new SegmentFilterParser(new SecurityGuard());
290
+ const expr = {
291
+ conditions: [{ field: '@', operator: '==' as const, value: 0, func: 'values' }],
292
+ logicals: [] as string[],
293
+ };
294
+
295
+ expect(parser.evaluate({ x: 1 }, expr)).toBe(true);
296
+ });
297
+
298
+ it('evaluates starts_with funcArgs[0] fallback to @', () => {
299
+ const parser = new SegmentFilterParser(new SecurityGuard());
300
+ const expr = {
301
+ conditions: [
302
+ {
303
+ field: '@',
304
+ operator: '==' as const,
305
+ value: true,
306
+ func: 'starts_with',
307
+ funcArgs: [] as string[],
308
+ },
309
+ ],
310
+ logicals: [] as string[],
311
+ };
312
+
313
+ expect(parser.evaluate({ x: 1 }, expr)).toBe(false);
314
+ });
315
+
316
+ it('evaluates contains funcArgs[0] fallback to @', () => {
317
+ const parser = new SegmentFilterParser(new SecurityGuard());
318
+ const expr = {
319
+ conditions: [
320
+ {
321
+ field: '@',
322
+ operator: '==' as const,
323
+ value: true,
324
+ func: 'contains',
325
+ funcArgs: [] as string[],
326
+ },
327
+ ],
328
+ logicals: [] as string[],
329
+ };
330
+
331
+ expect(parser.evaluate({ x: 1 }, expr)).toBe(false);
332
+ });
333
+
334
+ it('evaluates values funcArgs[0] fallback to @', () => {
335
+ const parser = new SegmentFilterParser(new SecurityGuard());
336
+ const expr = {
337
+ conditions: [
338
+ {
339
+ field: '@',
340
+ operator: '==' as const,
341
+ value: 0,
342
+ func: 'values',
343
+ funcArgs: [] as string[],
344
+ },
345
+ ],
346
+ logicals: [] as string[],
347
+ };
348
+
349
+ expect(parser.evaluate({ x: 1 }, expr)).toBe(true);
350
+ });
351
+
352
+ it('arithmetic regex rejects multi-operator expressions', () => {
353
+ const parser = new SegmentFilterParser(new SecurityGuard());
354
+
355
+ const expr = parser.parse('@.a + @.b - @.c > 0');
356
+
357
+ expect(parser.evaluate({ a: 10, b: 5, c: 3 }, expr)).toBe(false);
358
+ });
359
+
360
+ it('resolves plain field name without @. prefix in arithmetic', () => {
361
+ const parser = new SegmentFilterParser(new SecurityGuard());
362
+
363
+ const expr = parser.parse('price * qty > 100');
364
+
365
+ expect(parser.evaluate({ price: 50, qty: 3 }, expr)).toBe(true);
366
+ });
367
+
368
+ it('resolves field with @ prefix as whole item reference', () => {
369
+ const parser = new SegmentFilterParser(new SecurityGuard());
370
+
371
+ const expr = parser.parse("contains(@, 'hello')");
372
+
373
+ expect(parser.evaluate({ x: 1 } as unknown as Record<string, unknown>, expr)).toBe(false);
374
+ });
375
+
376
+ it('evaluates numeric string field value in arithmetic comparison', () => {
377
+ const parser = new SegmentFilterParser(new SecurityGuard());
378
+
379
+ const expr = parser.parse('@.a + @.b == 15');
380
+
381
+ expect(parser.evaluate({ a: '10', b: '5' }, expr)).toBe(true);
382
+ });
383
+ });
384
+
385
+ describe(`${SegmentFilterParser.name} > mutation killing - splitLogical stringChar`, () => {
386
+ it('tracks single-quoted strings across && logical operator', () => {
387
+ const parser = new SegmentFilterParser(new SecurityGuard());
388
+
389
+ const expr = parser.parse("name=='a' && age>1");
390
+
391
+ expect(expr.conditions).toHaveLength(2);
392
+ expect(expr.conditions[0].value).toBe('a');
393
+ expect(expr.logicals).toEqual(['&&']);
394
+ });
395
+
396
+ it('tracks single-quoted strings across || logical operator', () => {
397
+ const parser = new SegmentFilterParser(new SecurityGuard());
398
+
399
+ const expr = parser.parse("name=='x' || name=='y'");
400
+
401
+ expect(expr.conditions).toHaveLength(2);
402
+ expect(expr.logicals).toEqual(['||']);
403
+ });
404
+
405
+ it('tracks double-quoted strings followed by logical operator', () => {
406
+ const parser = new SegmentFilterParser(new SecurityGuard());
407
+
408
+ const expr = parser.parse('name=="val" && age>0');
409
+
410
+ expect(expr.conditions).toHaveLength(2);
411
+ expect(expr.conditions[0].value).toBe('val');
412
+ });
413
+ });
414
+
415
+ describe(`${SegmentFilterParser.name} > mutation killing - funcCompare regex anchors`, () => {
416
+ it('matches function comparison only at start of string (^ anchor)', () => {
417
+ const parser = new SegmentFilterParser(new SecurityGuard());
418
+
419
+ const expr = parser.parse("starts_with(@.name, 'A') == true");
420
+
421
+ expect(expr.conditions[0].func).toBe('starts_with');
422
+ expect(expr.conditions[0].operator).toBe('==');
423
+ expect(expr.conditions[0].value).toBe(true);
424
+ });
425
+
426
+ it('matches function comparison only at end of string ($ anchor)', () => {
427
+ const parser = new SegmentFilterParser(new SecurityGuard());
428
+
429
+ const expr = parser.parse('values(@.items) >= 3');
430
+
431
+ expect(expr.conditions[0].func).toBe('values');
432
+ expect(expr.conditions[0].operator).toBe('>=');
433
+ expect(expr.conditions[0].value).toBe(3);
434
+ });
435
+
436
+ it('requires whitespace before operator in function comparison (\\s not \\S)', () => {
437
+ const parser = new SegmentFilterParser(new SecurityGuard());
438
+
439
+ const expr = parser.parse('values(@.items) > 0');
440
+
441
+ expect(expr.conditions[0].func).toBe('values');
442
+ expect(expr.conditions[0].operator).toBe('>');
443
+ });
444
+
445
+ it('trims trailing whitespace from function comparison value', () => {
446
+ const parser = new SegmentFilterParser(new SecurityGuard());
447
+
448
+ const expr = parser.parse("starts_with(@.key, 'x') > 0 ");
449
+
450
+ expect(expr.conditions[0].value).toBe(0);
451
+ });
452
+
453
+ it('parses funcCompareMatch[4] after trimming', () => {
454
+ const parser = new SegmentFilterParser(new SecurityGuard());
455
+
456
+ const expr = parser.parse('values(@.items) == 5');
457
+
458
+ expect(expr.conditions[0].value).toBe(5);
459
+ });
460
+ });
461
+
462
+ describe(`${SegmentFilterParser.name} > mutation killing - funcBool regex`, () => {
463
+ it('matches boolean function only at end of string ($ anchor)', () => {
464
+ const parser = new SegmentFilterParser(new SecurityGuard());
465
+
466
+ const expr = parser.parse("starts_with(@.x, 'a')");
467
+
468
+ expect(expr.conditions[0].func).toBe('starts_with');
469
+ expect(expr.conditions[0].operator).toBe('==');
470
+ expect(expr.conditions[0].value).toBe(true);
471
+ });
472
+ });
473
+
474
+ describe(`${SegmentFilterParser.name} > mutation killing - parseValueDefault`, () => {
475
+ it('returns empty string as raw string (not number)', () => {
476
+ const parser = new SegmentFilterParser(new SecurityGuard());
477
+
478
+ const expr = parser.parse("key == ''");
479
+
480
+ expect(expr.conditions[0].value).toBe('');
481
+ });
482
+ });
483
+
484
+ describe(`${SegmentFilterParser.name} > mutation killing - arithmetic regex`, () => {
485
+ it('detects arithmetic with single-char operand at start', () => {
486
+ const parser = new SegmentFilterParser(new SecurityGuard());
487
+
488
+ const expr = parser.parse('@.a + @.b > 0');
489
+
490
+ expect(parser.evaluate({ a: 1, b: 2 }, expr)).toBe(true);
491
+ });
492
+
493
+ it('detects arithmetic with single-char operand at end', () => {
494
+ const parser = new SegmentFilterParser(new SecurityGuard());
495
+
496
+ const expr = parser.parse('@.x * 2 > 5');
497
+
498
+ expect(parser.evaluate({ x: 3 }, expr)).toBe(true);
499
+ expect(parser.evaluate({ x: 2 }, expr)).toBe(false);
500
+ });
501
+
502
+ it('detects arithmetic only with word-like operands (not uppercase W)', () => {
503
+ const parser = new SegmentFilterParser(new SecurityGuard());
504
+
505
+ const expr = parser.parse('@.price + @.tax > 100');
506
+
507
+ expect(parser.evaluate({ price: 80, tax: 25 }, expr)).toBe(true);
508
+ });
509
+
510
+ it('resolves integer literal on right side of arithmetic', () => {
511
+ const parser = new SegmentFilterParser(new SecurityGuard());
512
+
513
+ const expr = parser.parse('@.val + 5 == 10');
514
+
515
+ expect(parser.evaluate({ val: 5 }, expr)).toBe(true);
516
+ });
517
+
518
+ it('resolves float literal on right side of arithmetic', () => {
519
+ const parser = new SegmentFilterParser(new SecurityGuard());
520
+
521
+ const expr = parser.parse('@.val + 1.5 == 3.5');
522
+
523
+ expect(parser.evaluate({ val: 2 }, expr)).toBe(true);
524
+ });
525
+
526
+ it('resolves single-digit literal on right side of arithmetic', () => {
527
+ const parser = new SegmentFilterParser(new SecurityGuard());
528
+
529
+ const expr = parser.parse('@.val * 3 == 9');
530
+
531
+ expect(parser.evaluate({ val: 3 }, expr)).toBe(true);
532
+ });
533
+
534
+ it('resolves float with single decimal digit on right side', () => {
535
+ const parser = new SegmentFilterParser(new SecurityGuard());
536
+
537
+ const expr = parser.parse('@.val + 0.5 == 1.5');
538
+
539
+ expect(parser.evaluate({ val: 1 }, expr)).toBe(true);
540
+ });
541
+
542
+ it('arithmetic detection regex requires word chars (not \\W)', () => {
543
+ const parser = new SegmentFilterParser(new SecurityGuard());
544
+
545
+ const expr = parser.parse('@.a + @.b > 0');
546
+
547
+ expect(parser.evaluate({ a: 5, b: 5 }, expr)).toBe(true);
548
+ });
549
+ });
550
+
551
+ describe(`${SegmentFilterParser.name} > mutation killing - evaluateFunction switch`, () => {
552
+ it('dispatches starts_with correctly (not contains)', () => {
553
+ const parser = new SegmentFilterParser(new SecurityGuard());
554
+
555
+ const expr = parser.parse("starts_with(@.name, 'Al')");
556
+
557
+ expect(parser.evaluate({ name: 'Alice' }, expr)).toBe(true);
558
+ expect(parser.evaluate({ name: 'xAlice' }, expr)).toBe(false);
559
+ });
560
+
561
+ it('dispatches contains correctly (not starts_with)', () => {
562
+ const parser = new SegmentFilterParser(new SecurityGuard());
563
+
564
+ const expr = parser.parse("contains(@.name, 'li')");
565
+
566
+ expect(parser.evaluate({ name: 'Alice' }, expr)).toBe(true);
567
+ expect(parser.evaluate({ name: 'Bob' }, expr)).toBe(false);
568
+ });
569
+ });
570
+
571
+ describe(`${SegmentFilterParser.name} > mutation killing - evalStartsWith prefix`, () => {
572
+ it('uses funcArgs[1] as prefix when available', () => {
573
+ const parser = new SegmentFilterParser(new SecurityGuard());
574
+
575
+ const expr = parser.parse("starts_with(@.name, 'B')");
576
+
577
+ expect(parser.evaluate({ name: 'Bob' }, expr)).toBe(true);
578
+ expect(parser.evaluate({ name: 'Alice' }, expr)).toBe(false);
579
+ });
580
+
581
+ it('falls back to empty prefix when funcArgs[1] is missing', () => {
582
+ const parser = new SegmentFilterParser(new SecurityGuard());
583
+ const expr = {
584
+ conditions: [
585
+ {
586
+ field: '@.name',
587
+ operator: '==' as const,
588
+ value: true,
589
+ func: 'starts_with',
590
+ funcArgs: ['@.name'],
591
+ },
592
+ ],
593
+ logicals: [] as string[],
594
+ };
595
+
596
+ expect(parser.evaluate({ name: 'anything' }, expr)).toBe(true);
597
+ });
598
+
599
+ it('returns false when field is not a string for starts_with', () => {
600
+ const parser = new SegmentFilterParser(new SecurityGuard());
601
+ const expr = parser.parse("starts_with(@.val, 'x')");
602
+
603
+ expect(parser.evaluate({ val: 42 }, expr)).toBe(false);
604
+ });
605
+ });
606
+
607
+ describe(`${SegmentFilterParser.name} > mutation killing - evalContains needle`, () => {
608
+ it('uses funcArgs[1] as needle for contains', () => {
609
+ const parser = new SegmentFilterParser(new SecurityGuard());
610
+
611
+ const expr = parser.parse("contains(@.text, 'world')");
612
+
613
+ expect(parser.evaluate({ text: 'hello world' }, expr)).toBe(true);
614
+ expect(parser.evaluate({ text: 'hello' }, expr)).toBe(false);
615
+ });
616
+
617
+ it('falls back to empty needle when funcArgs[1] is missing', () => {
618
+ const parser = new SegmentFilterParser(new SecurityGuard());
619
+ const expr = {
620
+ conditions: [
621
+ {
622
+ field: '@.text',
623
+ operator: '==' as const,
624
+ value: true,
625
+ func: 'contains',
626
+ funcArgs: ['@.text'],
627
+ },
628
+ ],
629
+ logicals: [] as string[],
630
+ };
631
+
632
+ expect(parser.evaluate({ text: 'anything' }, expr)).toBe(true);
633
+ });
634
+ });
635
+
636
+ describe(`${SegmentFilterParser.name} > mutation killing - evalValues fallback`, () => {
637
+ it('returns 0 for non-array fields in values()', () => {
638
+ const parser = new SegmentFilterParser(new SecurityGuard());
639
+
640
+ const expr = parser.parse('values(@.name) == 0');
641
+
642
+ expect(parser.evaluate({ name: 'Alice' }, expr)).toBe(true);
643
+ });
644
+ });
645
+
646
+ describe(`${SegmentFilterParser.name} > mutation killing - resolveFilterArg`, () => {
647
+ it('resolves empty string arg as item reference', () => {
648
+ const parser = new SegmentFilterParser(new SecurityGuard());
649
+ const expr = {
650
+ conditions: [
651
+ {
652
+ field: '',
653
+ operator: '==' as const,
654
+ value: 0,
655
+ func: 'values',
656
+ funcArgs: [''],
657
+ },
658
+ ],
659
+ logicals: [] as string[],
660
+ };
661
+
662
+ expect(parser.evaluate({ x: 1 }, expr)).toBe(true);
663
+ });
664
+
665
+ it('resolves @ as whole item in function arg', () => {
666
+ const parser = new SegmentFilterParser(new SecurityGuard());
667
+ const expr = {
668
+ conditions: [
669
+ {
670
+ field: '@',
671
+ operator: '==' as const,
672
+ value: 0,
673
+ func: 'values',
674
+ funcArgs: ['@'],
675
+ },
676
+ ],
677
+ logicals: [] as string[],
678
+ };
679
+
680
+ expect(parser.evaluate({ x: 1 }, expr)).toBe(true);
681
+ });
682
+
683
+ it('resolves @.field by stripping prefix in function arg', () => {
684
+ const parser = new SegmentFilterParser(new SecurityGuard());
685
+
686
+ const expr = parser.parse("starts_with(@.name, 'Al')");
687
+
688
+ expect(parser.evaluate({ name: 'Alice' }, expr)).toBe(true);
689
+ expect(parser.evaluate({ name: 'Bob' }, expr)).toBe(false);
690
+ });
691
+ });
692
+
693
+ describe(`${SegmentFilterParser.name} > mutation killing - toNumber in arithmetic`, () => {
694
+ it('resolves @ prefix as item reference in arithmetic', () => {
695
+ const parser = new SegmentFilterParser(new SecurityGuard());
696
+
697
+ const expr = parser.parse('@.price * @.qty > 0');
698
+
699
+ expect(parser.evaluate({ price: 5, qty: 3 }, expr)).toBe(true);
700
+ });
701
+
702
+ it('uses startsWith @ to distinguish field from literal', () => {
703
+ const parser = new SegmentFilterParser(new SecurityGuard());
704
+
705
+ const expr = parser.parse('price + 10 > 20');
706
+
707
+ expect(parser.evaluate({ price: 15 }, expr)).toBe(true);
708
+ expect(parser.evaluate({ price: 5 }, expr)).toBe(false);
709
+ });
710
+
711
+ it('resolves multi-digit integer literal on right side of arithmetic', () => {
712
+ const parser = new SegmentFilterParser(new SecurityGuard());
713
+
714
+ const expr = parser.parse('@.val + 10 == 15');
715
+
716
+ expect(parser.evaluate({ val: 5 }, expr)).toBe(true);
717
+ expect(parser.evaluate({ val: 6 }, expr)).toBe(false);
718
+ });
719
+
720
+ it('resolves multi-digit float literal on right side of arithmetic', () => {
721
+ const parser = new SegmentFilterParser(new SecurityGuard());
722
+
723
+ const expr = parser.parse('@.val + 1.55 == 3.55');
724
+
725
+ expect(parser.evaluate({ val: 2 }, expr)).toBe(true);
726
+ });
727
+
728
+ it('resolves integer literal without decimal (matches \\d+ not requiring \\d+\\.\\d+)', () => {
729
+ const parser = new SegmentFilterParser(new SecurityGuard());
730
+
731
+ const expr = parser.parse('@.x * 5 == 25');
732
+
733
+ expect(parser.evaluate({ x: 5 }, expr)).toBe(true);
734
+ });
735
+ });