@safeaccess/inline 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/.gitattributes +1 -1
  2. package/CHANGELOG.md +23 -5
  3. package/LICENSE +1 -1
  4. package/README.md +79 -21
  5. package/dist/accessors/abstract-accessor.d.ts +24 -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 +26 -56
  67. package/dist/inline.js +43 -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 +4 -1
  82. package/dist/security/security-guard.js +7 -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 +25 -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 +46 -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 +10 -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 +379 -0
  163. package/tests/parser/xml-parser.test.ts +17 -330
  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 +8 -479
  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
@@ -7,6 +7,8 @@ import { SecurityException } from '../exceptions/security-exception.js';
7
7
  * Validates payload size, maximum key count, recursion depth, and
8
8
  * structural depth limits.
9
9
  *
10
+ * @api
11
+ *
10
12
  * @example
11
13
  * const parser = new SecurityParser({ maxDepth: 10, maxKeys: 100 });
12
14
  * parser.assertPayloadSize('{"key":"value"}');
@@ -27,7 +29,7 @@ export class SecurityParser implements SecurityParserInterface {
27
29
  * @param options.maxKeys - Maximum total number of keys across the entire structure. Default: 10000.
28
30
  * This value is also passed to `XmlParser` as the element-count cap for the Node.js manual XML
29
31
  * parser path. Setting it below a document's element count will cause `fromXml()` to throw
30
- * `SecurityException`. Non-positive or non-finite values disable that guard prefer the default.
32
+ * `SecurityException`. Non-positive or non-finite values disable that guard - prefer the default.
31
33
  * @param options.maxCountRecursiveDepth - Maximum recursion depth when counting keys. Default: 100.
32
34
  * @param options.maxResolveDepth - Maximum recursion depth for path resolution. Default: 100.
33
35
  */
@@ -41,9 +43,15 @@ export class SecurityParser implements SecurityParserInterface {
41
43
  } = {},
42
44
  ) {
43
45
  this.maxDepth = SecurityParser.clampOption(options.maxDepth, 512);
44
- this.maxPayloadBytes = SecurityParser.clampOption(options.maxPayloadBytes, 10 * 1024 * 1024);
46
+ this.maxPayloadBytes = SecurityParser.clampOption(
47
+ options.maxPayloadBytes,
48
+ 10 * 1024 * 1024,
49
+ );
45
50
  this.maxKeys = SecurityParser.clampOption(options.maxKeys, 10_000);
46
- this.maxCountRecursiveDepth = SecurityParser.clampOption(options.maxCountRecursiveDepth, 100);
51
+ this.maxCountRecursiveDepth = SecurityParser.clampOption(
52
+ options.maxCountRecursiveDepth,
53
+ 100,
54
+ );
47
55
  this.maxResolveDepth = SecurityParser.clampOption(options.maxResolveDepth, 100);
48
56
  }
49
57
 
@@ -226,6 +234,13 @@ export class SecurityParser implements SecurityParserInterface {
226
234
  return max;
227
235
  }
228
236
 
237
+ /**
238
+ * Clamp an optional numeric option to its default when not finite.
239
+ *
240
+ * @param value - User-provided option value.
241
+ * @param defaultValue - Fallback when value is undefined or non-finite.
242
+ * @returns Clamped numeric value.
243
+ */
229
244
  private static clampOption(value: number | undefined, defaultValue: number): number {
230
245
  /* Stryker disable next-line ConditionalExpression -- equivalent: Number.isFinite covers undefined (isFinite(undefined)===false); simplest safe form */
231
246
  return Number.isFinite(value) ? (value as number) : defaultValue;
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Used by {@link Inline.from} to select the appropriate accessor.
5
5
  *
6
+ * @api
7
+ *
6
8
  * @example
7
9
  * const accessor = Inline.from(TypeFormat.Json, '{"key":"value"}');
8
10
  */
@@ -6,19 +6,17 @@
6
6
  "!src/index.ts",
7
7
  "!src/contracts/**/*.ts",
8
8
  "!src/exceptions/**/*.ts",
9
- "!src/security/forbidden-keys.ts",
10
- "!src/parser/yaml-parser.ts",
11
- "!src/parser/xml-parser.ts"
9
+ "!src/security/forbidden-keys.ts"
12
10
  ],
13
11
  "thresholds": {
14
12
  "high": 100,
15
- "low": 100,
16
- "break": 100
13
+ "low": 90,
14
+ "break": 93
17
15
  },
18
- "timeoutMS": 60000,
19
- "concurrency": 4,
20
- "reporters": ["html", "clear-text", "progress"],
21
- "htmlReporter": {
22
- "fileName": "reports/stryker/mutation.html"
16
+ "timeoutMS": 30000,
17
+ "coverageAnalysis": "perTest",
18
+ "reporters": ["json", "clear-text", "progress"],
19
+ "jsonReporter": {
20
+ "fileName": "reports/mutation/mutation.json"
23
21
  }
24
22
  }
@@ -0,0 +1,217 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { AbstractAccessor } from '../../src/accessors/abstract-accessor.js';
3
+ import { JsonAccessor } from '../../src/accessors/formats/json-accessor.js';
4
+ import { DotNotationParser } from '../../src/core/dot-notation-parser.js';
5
+ import { SecurityGuard } from '../../src/security/security-guard.js';
6
+ import { SecurityParser } from '../../src/security/security-parser.js';
7
+ import { SecurityException } from '../../src/exceptions/security-exception.js';
8
+ import { ReadonlyViolationException } from '../../src/exceptions/readonly-violation-exception.js';
9
+ import { PathNotFoundException } from '../../src/exceptions/path-not-found-exception.js';
10
+
11
+ function makeParser(secParser?: SecurityParser): DotNotationParser {
12
+ return new DotNotationParser(new SecurityGuard(), secParser ?? new SecurityParser());
13
+ }
14
+
15
+ describe(AbstractAccessor.name, () => {
16
+ it('strict mode default is enabled - validates on ingest', () => {
17
+ expect(() => new JsonAccessor(makeParser()).from('{"__proto__": "bad"}')).toThrow(
18
+ SecurityException,
19
+ );
20
+ });
21
+
22
+ it('strict(false) disables validation', () => {
23
+ const a = new JsonAccessor(makeParser()).strict(false).from('{"__proto__": "ok"}');
24
+ expect(a.get('__proto__')).toBe('ok');
25
+ });
26
+
27
+ it('strict(true) re-enables validation', () => {
28
+ const accessor = new JsonAccessor(makeParser()).strict(false);
29
+ const strictAgain = accessor.strict(true);
30
+ expect(() => strictAgain.from('{"__proto__": "bad"}')).toThrow(SecurityException);
31
+ });
32
+
33
+ it('readonly(true) blocks set()', () => {
34
+ const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
35
+ expect(() => a.set('x', 2)).toThrow(ReadonlyViolationException);
36
+ });
37
+
38
+ it('readonly(true) blocks remove()', () => {
39
+ const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
40
+ expect(() => a.remove('x')).toThrow(ReadonlyViolationException);
41
+ });
42
+
43
+ it('readonly(false) allows mutation after readonly(true)', () => {
44
+ const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true).readonly(false);
45
+ expect(() => a.set('x', 2)).not.toThrow();
46
+ });
47
+
48
+ it('merge() combines two objects at root level', () => {
49
+ const a = new JsonAccessor(makeParser()).from('{"a":1}');
50
+ const merged = a.merge('', { b: 2 });
51
+ expect(merged.get('a')).toBe(1);
52
+ expect(merged.get('b')).toBe(2);
53
+ });
54
+
55
+ it('merge() at a nested path', () => {
56
+ const a = new JsonAccessor(makeParser()).from('{"user":{"name":"Alice"}}');
57
+ const merged = a.merge('user', { role: 'admin' });
58
+ expect(merged.get('user.name')).toBe('Alice');
59
+ expect(merged.get('user.role')).toBe('admin');
60
+ });
61
+
62
+ it('all() returns all parsed data', () => {
63
+ const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2}');
64
+ expect(a.all()).toEqual({ a: 1, b: 2 });
65
+ });
66
+
67
+ it('keys() returns root-level keys', () => {
68
+ const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2}');
69
+ expect(a.keys()).toEqual(['a', 'b']);
70
+ });
71
+
72
+ it('count() returns number of root keys', () => {
73
+ const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2,"c":3}');
74
+ expect(a.count()).toBe(3);
75
+ });
76
+
77
+ it('getRaw() returns original input', () => {
78
+ const json = '{"name":"Alice"}';
79
+ expect(new JsonAccessor(makeParser()).from(json).getRaw()).toBe(json);
80
+ });
81
+
82
+ it('getOrFail() throws PathNotFoundException for missing path', () => {
83
+ const a = new JsonAccessor(makeParser()).from('{}');
84
+ expect(() => a.getOrFail('missing')).toThrow(PathNotFoundException);
85
+ });
86
+
87
+ it('readonly() with no argument defaults to true (blocks mutations)', () => {
88
+ const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly();
89
+ expect(() => a.set('x', 2)).toThrow(ReadonlyViolationException);
90
+ });
91
+
92
+ it('strict() with no argument defaults to true (enables validation)', () => {
93
+ const accessor = new JsonAccessor(makeParser()).strict(false);
94
+ const strictAgain = accessor.strict();
95
+ expect(() => strictAgain.from('{"__proto__": "bad"}')).toThrow(SecurityException);
96
+ });
97
+
98
+ it('getAt() returns null when path does not exist (default is null)', () => {
99
+ const a = new JsonAccessor(makeParser()).from('{"a":1}');
100
+ expect(a.getAt(['missing'])).toBeNull();
101
+ });
102
+
103
+ it('getAt() resolves a value using pre-parsed segments', () => {
104
+ const a = new JsonAccessor(makeParser()).from('{"user":{"name":"Alice"}}');
105
+ expect(a.getAt(['user', 'name'])).toBe('Alice');
106
+ });
107
+
108
+ it('hasAt() returns true when segments resolve to a value', () => {
109
+ const a = new JsonAccessor(makeParser()).from('{"a":{"b":1}}');
110
+ expect(a.hasAt(['a', 'b'])).toBe(true);
111
+ });
112
+
113
+ it('hasAt() returns false when segments do not resolve', () => {
114
+ const a = new JsonAccessor(makeParser()).from('{"a":1}');
115
+ expect(a.hasAt(['missing'])).toBe(false);
116
+ });
117
+
118
+ it('setAt() sets a value using pre-parsed segments', () => {
119
+ const a = new JsonAccessor(makeParser()).from('{}');
120
+ const updated = a.setAt(['user', 'name'], 'Alice');
121
+ expect(updated.get('user.name')).toBe('Alice');
122
+ });
123
+
124
+ it('setAt() throws ReadonlyViolationException when readonly', () => {
125
+ const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
126
+ expect(() => a.setAt(['x'], 2)).toThrow(ReadonlyViolationException);
127
+ });
128
+
129
+ it('removeAt() removes a value using pre-parsed segments', () => {
130
+ const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2}');
131
+ const updated = a.removeAt(['a']);
132
+ expect(updated.has('a')).toBe(false);
133
+ expect(updated.has('b')).toBe(true);
134
+ });
135
+
136
+ it('removeAt() throws ReadonlyViolationException when readonly', () => {
137
+ const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
138
+ expect(() => a.removeAt(['x'])).toThrow(ReadonlyViolationException);
139
+ });
140
+
141
+ it('getMany() returns map of paths to their values', () => {
142
+ const a = new JsonAccessor(makeParser()).from('{"a":1,"b":2}');
143
+ expect(a.getMany({ a: 0, b: 0, missing: 'fallback' })).toEqual({
144
+ a: 1,
145
+ b: 2,
146
+ missing: 'fallback',
147
+ });
148
+ });
149
+
150
+ it('count(path) returns number of keys at a nested path', () => {
151
+ const a = new JsonAccessor(makeParser()).from('{"user":{"name":"Alice","age":30}}');
152
+ expect(a.count('user')).toBe(2);
153
+ });
154
+
155
+ it('count(path) returns 0 when path resolves to a non-object', () => {
156
+ const a = new JsonAccessor(makeParser()).from('{"a":"string"}');
157
+ expect(a.count('a')).toBe(0);
158
+ });
159
+
160
+ it('keys(path) returns keys at a nested path', () => {
161
+ const a = new JsonAccessor(makeParser()).from('{"user":{"name":"Alice","role":"admin"}}');
162
+ expect(a.keys('user')).toEqual(['name', 'role']);
163
+ });
164
+
165
+ it('keys(path) returns empty array when path resolves to a non-object', () => {
166
+ const a = new JsonAccessor(makeParser()).from('{"a":42}');
167
+ expect(a.keys('a')).toEqual([]);
168
+ });
169
+
170
+ it('mergeAll() deep-merges into root', () => {
171
+ const a = new JsonAccessor(makeParser()).from('{"a":1}');
172
+ const merged = a.mergeAll({ b: 2, c: 3 });
173
+ expect(merged.get('a')).toBe(1);
174
+ expect(merged.get('b')).toBe(2);
175
+ expect(merged.get('c')).toBe(3);
176
+ });
177
+
178
+ it('mergeAll() throws ReadonlyViolationException when readonly', () => {
179
+ const a = new JsonAccessor(makeParser()).from('{"a":1}').readonly(true);
180
+ expect(() => a.mergeAll({ b: 2 })).toThrow(ReadonlyViolationException);
181
+ });
182
+
183
+ it('set() clone preserves readonly state', () => {
184
+ const a = new JsonAccessor(makeParser()).from('{"x":1}').readonly(true);
185
+ const b = a.readonly(false).set('x', 2);
186
+ expect(b.get('x')).toBe(2);
187
+ });
188
+
189
+ it('set() clone inherits strict mode - security validation still enforced', () => {
190
+ const a = new JsonAccessor(makeParser()).from('{"x":1}');
191
+ const b = a.set('x', 2);
192
+ expect(() => b.from('{"__proto__":"bad"}')).toThrow(SecurityException);
193
+ });
194
+
195
+ it('keys() returns [] when path resolves to null (typeof null is object in JS)', () => {
196
+ const a = new JsonAccessor(makeParser()).from('{"a":null}');
197
+ expect(a.keys('a')).toEqual([]);
198
+ });
199
+
200
+ it('count() returns 0 when path resolves to null (typeof null is object in JS)', () => {
201
+ const a = new JsonAccessor(makeParser()).from('{"a":null}');
202
+ expect(a.count('a')).toBe(0);
203
+ });
204
+
205
+ it('strict(false) bypasses payload size validation', () => {
206
+ const tinyParser = new SecurityParser({ maxPayloadBytes: 5 });
207
+ const parser = new DotNotationParser(new SecurityGuard(), tinyParser);
208
+ const a = new JsonAccessor(parser).strict(false).from('{"name":"Alice"}');
209
+ expect(a.get('name')).toBe('Alice');
210
+ });
211
+
212
+ it('strict(true) enforces payload size validation', () => {
213
+ const tinyParser = new SecurityParser({ maxPayloadBytes: 5 });
214
+ const parser = new DotNotationParser(new SecurityGuard(), tinyParser);
215
+ expect(() => new JsonAccessor(parser).from('{"name":"Alice"}')).toThrow(SecurityException);
216
+ });
217
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { AbstractIntegrationAccessor } from '../../src/accessors/abstract-integration-accessor.js';
3
+ import type { ParseIntegrationInterface } from '../../src/contracts/parse-integration-interface.js';
4
+ import type { ValidatableParserInterface } from '../../src/contracts/validatable-parser-interface.js';
5
+ import { DotNotationParser } from '../../src/core/dot-notation-parser.js';
6
+ import { SecurityGuard } from '../../src/security/security-guard.js';
7
+ import { SecurityParser } from '../../src/security/security-parser.js';
8
+ import { FakeParseIntegration } from '../mocks/fake-parse-integration.js';
9
+
10
+ class ConcreteIntegrationAccessor extends AbstractIntegrationAccessor {
11
+ protected parse(raw: unknown): Record<string, unknown> {
12
+ return raw as Record<string, unknown>;
13
+ }
14
+ }
15
+
16
+ function makeParser(): ValidatableParserInterface {
17
+ return new DotNotationParser(new SecurityGuard(), new SecurityParser());
18
+ }
19
+
20
+ describe(AbstractIntegrationAccessor.name, () => {
21
+ it('accepts a ValidatableParserInterface and ParseIntegrationInterface', () => {
22
+ const parser = makeParser();
23
+ const integration = new FakeParseIntegration();
24
+ const accessor = new ConcreteIntegrationAccessor(parser, integration);
25
+
26
+ expect(accessor).toBeInstanceOf(AbstractIntegrationAccessor);
27
+ });
28
+
29
+ it('exposes the integration property to subclasses', () => {
30
+ const integration = new FakeParseIntegration(true, { key: 'value' });
31
+ const accessor = new ConcreteIntegrationAccessor(makeParser(), integration);
32
+
33
+ expect(
34
+ (accessor as unknown as { integration: ParseIntegrationInterface }).integration,
35
+ ).toBe(integration);
36
+ });
37
+ });
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { AnyAccessor } from '../../../src/accessors/formats/any-accessor.js';
3
+ import { DotNotationParser } from '../../../src/core/dot-notation-parser.js';
4
+ import { SecurityGuard } from '../../../src/security/security-guard.js';
5
+ import { SecurityParser } from '../../../src/security/security-parser.js';
6
+ import { InvalidFormatException } from '../../../src/exceptions/invalid-format-exception.js';
7
+ import { SecurityException } from '../../../src/exceptions/security-exception.js';
8
+ import { FakeParseIntegration } from '../../mocks/fake-parse-integration.js';
9
+
10
+ function makeParser(secParser?: SecurityParser): DotNotationParser {
11
+ return new DotNotationParser(new SecurityGuard(), secParser ?? new SecurityParser());
12
+ }
13
+
14
+ describe(AnyAccessor.name, () => {
15
+ it('accepts data when integration assertFormat returns true', () => {
16
+ const integration = new FakeParseIntegration(true, { key: 'value' });
17
+ const a = new AnyAccessor(makeParser(), integration).from('some data');
18
+ expect(a.get('key')).toBe('value');
19
+ });
20
+
21
+ it('throws InvalidFormatException when integration rejects format', () => {
22
+ const integration = new FakeParseIntegration(false, {});
23
+ expect(() => new AnyAccessor(makeParser(), integration).from('data')).toThrow(
24
+ InvalidFormatException,
25
+ );
26
+ });
27
+
28
+ it('validates string payloads through assertPayload', () => {
29
+ const secParser = new SecurityParser({ maxPayloadBytes: 3 });
30
+ const parser = makeParser(secParser);
31
+ const integration = new FakeParseIntegration(true, {});
32
+ expect(() => new AnyAccessor(parser, integration).from('1234')).toThrow(SecurityException);
33
+ });
34
+
35
+ it('does not call assertPayload for non-string data', () => {
36
+ const secParser = new SecurityParser({ maxPayloadBytes: 1 });
37
+ const parser = makeParser(secParser);
38
+ const integration = new FakeParseIntegration(true, { a: 1 });
39
+ expect(() => new AnyAccessor(parser, integration).from({ x: 1 })).not.toThrow();
40
+ });
41
+
42
+ it('resolves nested path from parsed data', () => {
43
+ const integration = new FakeParseIntegration(true, { user: { name: 'Alice' } });
44
+ const a = new AnyAccessor(makeParser(), integration).from('anything');
45
+ expect(a.get('user.name')).toBe('Alice');
46
+ });
47
+
48
+ it('error message mentions typeof when format is rejected', () => {
49
+ const integration = new FakeParseIntegration(false, {});
50
+ expect(() => new AnyAccessor(makeParser(), integration).from(42)).toThrow(/number/);
51
+ });
52
+
53
+ it('FakeParseIntegration default constructor accepts any input', () => {
54
+ const integration = new FakeParseIntegration();
55
+ expect(integration.assertFormat('test')).toBe(true);
56
+ });
57
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ArrayAccessor } from '../../../src/accessors/formats/array-accessor.js';
3
+ import { DotNotationParser } from '../../../src/core/dot-notation-parser.js';
4
+ import { SecurityGuard } from '../../../src/security/security-guard.js';
5
+ import { SecurityParser } from '../../../src/security/security-parser.js';
6
+ import { InvalidFormatException } from '../../../src/exceptions/invalid-format-exception.js';
7
+
8
+ function makeParser(secParser?: SecurityParser): DotNotationParser {
9
+ return new DotNotationParser(new SecurityGuard(), secParser ?? new SecurityParser());
10
+ }
11
+
12
+ describe(ArrayAccessor.name, () => {
13
+ it('accepts a plain object', () => {
14
+ const a = new ArrayAccessor(makeParser()).from({ key: 'value' });
15
+ expect(a.get('key')).toBe('value');
16
+ });
17
+
18
+ it('accepts an array, indexing by position', () => {
19
+ const a = new ArrayAccessor(makeParser()).from(['a', 'b', 'c']);
20
+ expect(a.get('0')).toBe('a');
21
+ expect(a.get('2')).toBe('c');
22
+ });
23
+
24
+ it('throws InvalidFormatException for string input', () => {
25
+ expect(() => new ArrayAccessor(makeParser()).from('string')).toThrow(
26
+ InvalidFormatException,
27
+ );
28
+ });
29
+
30
+ it('throws InvalidFormatException for null input', () => {
31
+ expect(() => new ArrayAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
32
+ });
33
+
34
+ it('throws InvalidFormatException for number input', () => {
35
+ expect(() => new ArrayAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
36
+ });
37
+
38
+ it('resolves a nested path in an array-ingested object', () => {
39
+ const a = new ArrayAccessor(makeParser()).from({ user: { name: 'Alice' } });
40
+ expect(a.get('user.name')).toBe('Alice');
41
+ });
42
+ });
@@ -0,0 +1,103 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { EnvAccessor } from '../../../src/accessors/formats/env-accessor.js';
3
+ import { DotNotationParser } from '../../../src/core/dot-notation-parser.js';
4
+ import { SecurityGuard } from '../../../src/security/security-guard.js';
5
+ import { SecurityParser } from '../../../src/security/security-parser.js';
6
+ import { InvalidFormatException } from '../../../src/exceptions/invalid-format-exception.js';
7
+
8
+ function makeParser(secParser?: SecurityParser): DotNotationParser {
9
+ return new DotNotationParser(new SecurityGuard(), secParser ?? new SecurityParser());
10
+ }
11
+
12
+ describe(EnvAccessor.name, () => {
13
+ it('parses KEY=VALUE pairs', () => {
14
+ const a = new EnvAccessor(makeParser()).from('DB_HOST=localhost\nPORT=5432');
15
+ expect(a.get('DB_HOST')).toBe('localhost');
16
+ expect(a.get('PORT')).toBe('5432');
17
+ });
18
+
19
+ it('skips comment lines', () => {
20
+ const a = new EnvAccessor(makeParser()).from('# comment\nKEY=value');
21
+ expect(a.has('# comment')).toBe(false);
22
+ expect(a.get('KEY')).toBe('value');
23
+ });
24
+
25
+ it('skips blank lines', () => {
26
+ const a = new EnvAccessor(makeParser()).from('\nKEY=value\n');
27
+ expect(a.get('KEY')).toBe('value');
28
+ });
29
+
30
+ it('strips double quotes from values', () => {
31
+ const a = new EnvAccessor(makeParser()).from('MSG="hello world"');
32
+ expect(a.get('MSG')).toBe('hello world');
33
+ });
34
+
35
+ it('strips single quotes from values', () => {
36
+ const a = new EnvAccessor(makeParser()).from("MSG='hello world'");
37
+ expect(a.get('MSG')).toBe('hello world');
38
+ });
39
+
40
+ it('skips lines without = sign', () => {
41
+ const a = new EnvAccessor(makeParser()).from('INVALID_LINE\nKEY=value');
42
+ expect(a.has('INVALID_LINE')).toBe(false);
43
+ expect(a.get('KEY')).toBe('value');
44
+ });
45
+
46
+ it('throws InvalidFormatException for non-string input', () => {
47
+ expect(() => new EnvAccessor(makeParser()).from(123)).toThrow(InvalidFormatException);
48
+ });
49
+
50
+ it('throws InvalidFormatException for null input', () => {
51
+ expect(() => new EnvAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
52
+ });
53
+
54
+ it('handles KEY= with no value (empty string)', () => {
55
+ const a = new EnvAccessor(makeParser()).from('EMPTY=');
56
+ expect(a.get('EMPTY')).toBe('');
57
+ });
58
+
59
+ it('handles value with multiple = signs', () => {
60
+ const a = new EnvAccessor(makeParser()).from('JWT=a=b=c');
61
+ expect(a.get('JWT')).toBe('a=b=c');
62
+ });
63
+
64
+ it('trims whitespace from key names', () => {
65
+ const a = new EnvAccessor(makeParser()).from(' KEY =value');
66
+ expect(a.get('KEY')).toBe('value');
67
+ });
68
+
69
+ it('trims whitespace from values', () => {
70
+ const a = new EnvAccessor(makeParser()).from('KEY= trimmed ');
71
+ expect(a.get('KEY')).toBe('trimmed');
72
+ });
73
+
74
+ it('does not strip double quotes when only one side is present', () => {
75
+ const a = new EnvAccessor(makeParser()).from('KEY=hello"');
76
+ expect(a.get('KEY')).toBe('hello"');
77
+ });
78
+
79
+ it('does not strip double quotes when value starts without quote', () => {
80
+ const a = new EnvAccessor(makeParser()).from('KEY="hello');
81
+ expect(a.get('KEY')).toBe('"hello');
82
+ });
83
+
84
+ it('does not strip single quotes when only one side is present', () => {
85
+ const a = new EnvAccessor(makeParser()).from("KEY=hello'");
86
+ expect(a.get('KEY')).toBe("hello'");
87
+ });
88
+
89
+ it('does not strip single quotes when value starts without quote', () => {
90
+ const a = new EnvAccessor(makeParser()).from("KEY='hello");
91
+ expect(a.get('KEY')).toBe("'hello");
92
+ });
93
+
94
+ it('skips lines starting with # even if they contain =', () => {
95
+ const a = new EnvAccessor(makeParser()).from('# KEY=value\nREAL=ok');
96
+ expect(a.has('# KEY')).toBe(false);
97
+ expect(a.get('REAL')).toBe('ok');
98
+ });
99
+
100
+ it('error message from from() includes the actual typeof data', () => {
101
+ expect(() => new EnvAccessor(makeParser()).from(42)).toThrow(/number/);
102
+ });
103
+ });