@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,186 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { IniAccessor } from '../../../src/accessors/formats/ini-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(IniAccessor.name, () => {
13
+ it('parses flat key=value pairs', () => {
14
+ const a = new IniAccessor(makeParser()).from('name=Alice\nage=30');
15
+ expect(a.get('name')).toBe('Alice');
16
+ expect(a.get('age')).toBe(30);
17
+ });
18
+
19
+ it('parses sections as nested keys', () => {
20
+ const a = new IniAccessor(makeParser()).from('[db]\nhost=localhost\nport=5432');
21
+ expect(a.get('db.host')).toBe('localhost');
22
+ expect(a.get('db.port')).toBe(5432);
23
+ });
24
+
25
+ it('throws InvalidFormatException for non-string input', () => {
26
+ expect(() => new IniAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
27
+ });
28
+
29
+ it('throws InvalidFormatException for number input', () => {
30
+ expect(() => new IniAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
31
+ });
32
+
33
+ it('skips comment lines starting with #', () => {
34
+ const a = new IniAccessor(makeParser()).from('# comment\nkey=value');
35
+ expect(a.has('# comment')).toBe(false);
36
+ expect(a.get('key')).toBe('value');
37
+ });
38
+
39
+ it('skips comment lines starting with ;', () => {
40
+ const a = new IniAccessor(makeParser()).from('; comment\nkey=value');
41
+ expect(a.get('key')).toBe('value');
42
+ });
43
+
44
+ it('skips blank lines', () => {
45
+ const a = new IniAccessor(makeParser()).from('\nkey=value\n');
46
+ expect(a.get('key')).toBe('value');
47
+ });
48
+
49
+ it('casts true, yes, on to boolean true', () => {
50
+ const a = new IniAccessor(makeParser()).from('a=true\nb=yes\nc=on');
51
+ expect(a.get('a')).toBe(true);
52
+ expect(a.get('b')).toBe(true);
53
+ expect(a.get('c')).toBe(true);
54
+ });
55
+
56
+ it('casts false, no, off, none to boolean false', () => {
57
+ const a = new IniAccessor(makeParser()).from('a=false\nb=no\nc=off\nd=none');
58
+ expect(a.get('a')).toBe(false);
59
+ expect(a.get('b')).toBe(false);
60
+ expect(a.get('c')).toBe(false);
61
+ expect(a.get('d')).toBe(false);
62
+ });
63
+
64
+ it('casts null and empty string to null', () => {
65
+ const a = new IniAccessor(makeParser()).from('a=null\nb=');
66
+ expect(a.get('a')).toBeNull();
67
+ expect(a.get('b')).toBeNull();
68
+ });
69
+
70
+ it('strips surrounding double quotes', () => {
71
+ const a = new IniAccessor(makeParser()).from('msg="hello world"');
72
+ expect(a.get('msg')).toBe('hello world');
73
+ });
74
+
75
+ it('strips surrounding single quotes', () => {
76
+ const a = new IniAccessor(makeParser()).from("msg='hello world'");
77
+ expect(a.get('msg')).toBe('hello world');
78
+ });
79
+
80
+ it('casts integer values', () => {
81
+ const a = new IniAccessor(makeParser()).from('port=3306');
82
+ expect(a.get('port')).toBe(3306);
83
+ });
84
+
85
+ it('casts float values', () => {
86
+ const a = new IniAccessor(makeParser()).from('ratio=3.14');
87
+ expect(a.get('ratio')).toBe(3.14);
88
+ });
89
+
90
+ it('skips lines without = sign', () => {
91
+ const a = new IniAccessor(makeParser()).from('badline\nkey=value');
92
+ expect(a.has('badline')).toBe(false);
93
+ });
94
+
95
+ it('parses multiple sections', () => {
96
+ const a = new IniAccessor(makeParser()).from('[app]\nname=MyApp\n[db]\nhost=localhost');
97
+ expect(a.get('app.name')).toBe('MyApp');
98
+ expect(a.get('db.host')).toBe('localhost');
99
+ });
100
+
101
+ it('error message from from() includes the actual typeof data', () => {
102
+ expect(() => new IniAccessor(makeParser()).from(42)).toThrow(/number/);
103
+ });
104
+
105
+ it('preserves existing section keys when section header appears twice', () => {
106
+ const a = new IniAccessor(makeParser()).from('[db]\nhost=localhost\n[db]\nport=5432');
107
+ expect(a.get('db.host')).toBe('localhost');
108
+ expect(a.get('db.port')).toBe(5432);
109
+ });
110
+
111
+ it('trims whitespace from key names and raw values', () => {
112
+ const a = new IniAccessor(makeParser()).from(' name = Alice ');
113
+ expect(a.get('name')).toBe('Alice');
114
+ });
115
+
116
+ it('parses section with underscored name', () => {
117
+ const a = new IniAccessor(makeParser()).from('[my_section]\nkey=val');
118
+ expect(a.get('my_section.key')).toBe('val');
119
+ });
120
+
121
+ it('does not strip double quotes when only one side is present', () => {
122
+ const a = new IniAccessor(makeParser()).from('key=hello"');
123
+ expect(a.get('key')).toBe('hello"');
124
+ });
125
+
126
+ it('does not strip single quotes when only one side is present', () => {
127
+ const a = new IniAccessor(makeParser()).from("key=hello'");
128
+ expect(a.get('key')).toBe("hello'");
129
+ });
130
+
131
+ it('does not strip double quotes when value starts without quote', () => {
132
+ const a = new IniAccessor(makeParser()).from('key="hello');
133
+ expect(a.get('key')).toBe('"hello');
134
+ });
135
+
136
+ it('does not strip single quotes when value starts without quote', () => {
137
+ const a = new IniAccessor(makeParser()).from("key='hello");
138
+ expect(a.get('key')).toBe("'hello");
139
+ });
140
+
141
+ it('does not cast string with trailing non-digit characters as integer', () => {
142
+ const a = new IniAccessor(makeParser()).from('version=3.0-beta');
143
+ expect(typeof a.get('version')).toBe('string');
144
+ });
145
+
146
+ it('does not cast string with leading non-digit chars as integer', () => {
147
+ const a = new IniAccessor(makeParser()).from('version=v42');
148
+ expect(typeof a.get('version')).toBe('string');
149
+ });
150
+
151
+ it('does not cast a version string like 3.1.4 as float', () => {
152
+ const a = new IniAccessor(makeParser()).from('ver=3.1.4');
153
+ expect(typeof a.get('ver')).toBe('string');
154
+ });
155
+
156
+ it('casts "yes" to boolean true', () => {
157
+ const a = new IniAccessor(makeParser()).from('flag=yes');
158
+ expect(a.get('flag')).toBe(true);
159
+ });
160
+
161
+ it('casts "no" to boolean false', () => {
162
+ const a = new IniAccessor(makeParser()).from('flag=no');
163
+ expect(a.get('flag')).toBe(false);
164
+ });
165
+
166
+ it('casts "none" to boolean false', () => {
167
+ const a = new IniAccessor(makeParser()).from('flag=none');
168
+ expect(a.get('flag')).toBe(false);
169
+ });
170
+
171
+ it('does not treat key=value containing [brackets] as section header', () => {
172
+ const a = new IniAccessor(makeParser()).from('key=value[brackets]\nother=1');
173
+ expect(a.get('key')).toBe('value[brackets]');
174
+ expect(a.get('other')).toBe(1);
175
+ });
176
+
177
+ it('casts a two-digit integer before decimal point to float', () => {
178
+ const a = new IniAccessor(makeParser()).from('ratio=10.5');
179
+ expect(a.get('ratio')).toBe(10.5);
180
+ });
181
+
182
+ it('casts negative float with two-digit integer part correctly', () => {
183
+ const a = new IniAccessor(makeParser()).from('offset=-12.75');
184
+ expect(a.get('offset')).toBe(-12.75);
185
+ });
186
+ });
@@ -1,9 +1,9 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { JsonAccessor } from '../../src/accessors/formats/json-accessor.js';
3
- import { DotNotationParser } from '../../src/core/dot-notation-parser.js';
4
- import { InvalidFormatException } from '../../src/exceptions/invalid-format-exception.js';
5
- import { PathNotFoundException } from '../../src/exceptions/path-not-found-exception.js';
6
- import { ReadonlyViolationException } from '../../src/exceptions/readonly-violation-exception.js';
2
+ import { JsonAccessor } from '../../../src/accessors/formats/json-accessor.js';
3
+ import { DotNotationParser } from '../../../src/core/dot-notation-parser.js';
4
+ import { InvalidFormatException } from '../../../src/exceptions/invalid-format-exception.js';
5
+ import { PathNotFoundException } from '../../../src/exceptions/path-not-found-exception.js';
6
+ import { ReadonlyViolationException } from '../../../src/exceptions/readonly-violation-exception.js';
7
7
 
8
8
  function makeAccessor(): JsonAccessor {
9
9
  return new JsonAccessor(new DotNotationParser());
@@ -142,7 +142,7 @@ describe(`${JsonAccessor.name} > getRaw`, () => {
142
142
  });
143
143
 
144
144
  describe(`${JsonAccessor.name} > parse non-object JSON`, () => {
145
- // Kills line 51:13/44/62 `typeof decoded !== 'object' || decoded === null` check
145
+ // Kills line 51:13/44/62 - `typeof decoded !== 'object' || decoded === null` check
146
146
  // JSON.parse('null') returns null → should return {}
147
147
  it('returns empty object when JSON decodes to null', () => {
148
148
  const accessor = makeAccessor().from('null');
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { NdjsonAccessor } from '../../../src/accessors/formats/ndjson-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(NdjsonAccessor.name, () => {
13
+ it('parses two NDJSON lines', () => {
14
+ const a = new NdjsonAccessor(makeParser()).from('{"id":1}\n{"id":2}');
15
+ expect(a.get('0.id')).toBe(1);
16
+ expect(a.get('1.id')).toBe(2);
17
+ });
18
+
19
+ it('throws InvalidFormatException for non-string input', () => {
20
+ expect(() => new NdjsonAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
21
+ });
22
+
23
+ it('throws InvalidFormatException for number input', () => {
24
+ expect(() => new NdjsonAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
25
+ });
26
+
27
+ it('returns empty object for blank-only input', () => {
28
+ const a = new NdjsonAccessor(makeParser()).from(' \n \n');
29
+ expect(a.all()).toEqual({});
30
+ });
31
+
32
+ it('skips blank lines between valid lines', () => {
33
+ const a = new NdjsonAccessor(makeParser()).from('{"id":1}\n\n{"id":2}');
34
+ expect(a.get('0.id')).toBe(1);
35
+ expect(a.get('1.id')).toBe(2);
36
+ });
37
+
38
+ it('throws InvalidFormatException for malformed JSON line', () => {
39
+ expect(() => new NdjsonAccessor(makeParser()).from('{"ok":1}\n{not valid}')).toThrow(
40
+ InvalidFormatException,
41
+ );
42
+ });
43
+
44
+ it('error message contains the failing line number', () => {
45
+ expect(() => new NdjsonAccessor(makeParser()).from('{"ok":1}\n{not valid}')).toThrow(
46
+ /line 2/,
47
+ );
48
+ });
49
+ });
@@ -0,0 +1,172 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ObjectAccessor } from '../../../src/accessors/formats/object-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
+
9
+ function makeParser(secParser?: SecurityParser): DotNotationParser {
10
+ return new DotNotationParser(new SecurityGuard(), secParser ?? new SecurityParser());
11
+ }
12
+
13
+ describe(ObjectAccessor.name, () => {
14
+ it('accepts a plain object', () => {
15
+ const a = new ObjectAccessor(makeParser()).from({ name: 'Alice' });
16
+ expect(a.get('name')).toBe('Alice');
17
+ });
18
+
19
+ it('throws InvalidFormatException for string input', () => {
20
+ expect(() => new ObjectAccessor(makeParser()).from('string')).toThrow(
21
+ InvalidFormatException,
22
+ );
23
+ });
24
+
25
+ it('throws InvalidFormatException for null input', () => {
26
+ expect(() => new ObjectAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
27
+ });
28
+
29
+ it('throws InvalidFormatException for array input', () => {
30
+ expect(() => new ObjectAccessor(makeParser()).from([1, 2, 3])).toThrow(
31
+ InvalidFormatException,
32
+ );
33
+ });
34
+
35
+ it('throws InvalidFormatException with "array" in message for array input', () => {
36
+ expect(() => new ObjectAccessor(makeParser()).from([])).toThrow(/array/);
37
+ });
38
+
39
+ it('resolves nested paths', () => {
40
+ const a = new ObjectAccessor(makeParser()).from({ user: { name: 'Bob' } });
41
+ expect(a.get('user.name')).toBe('Bob');
42
+ });
43
+
44
+ it('handles nested arrays of objects', () => {
45
+ const a = new ObjectAccessor(makeParser()).from({ items: [{ id: 1 }, { id: 2 }] });
46
+ expect(a.get('items')).toEqual([{ id: 1 }, { id: 2 }]);
47
+ });
48
+
49
+ it('throws SecurityException when depth exceeds the limit', () => {
50
+ const secParser = new SecurityParser({ maxDepth: 1 });
51
+ const parser = makeParser(secParser);
52
+ expect(() => new ObjectAccessor(parser).from({ a: { b: { c: 1 } } })).toThrow(
53
+ SecurityException,
54
+ );
55
+ });
56
+
57
+ it('does not throw for objects within the depth limit', () => {
58
+ const secParser = new SecurityParser({ maxDepth: 5 });
59
+ const parser = makeParser(secParser);
60
+ expect(() => new ObjectAccessor(parser).from({ a: { b: { c: 1 } } })).not.toThrow();
61
+ });
62
+
63
+ it('throws SecurityException for deeply nested arrays exceeding depth', () => {
64
+ const secParser = new SecurityParser({ maxDepth: 0 });
65
+ const parser = makeParser(secParser);
66
+ expect(() => new ObjectAccessor(parser).from({ items: [{ id: 1 }] })).toThrow(
67
+ SecurityException,
68
+ );
69
+ });
70
+
71
+ it('handles nested arrays of primitives without throwing', () => {
72
+ const a = new ObjectAccessor(makeParser()).from({ tags: ['a', 'b', 'c'] });
73
+ expect(a.get('tags')).toEqual(['a', 'b', 'c']);
74
+ });
75
+
76
+ it('preserves null values in object', () => {
77
+ const a = new ObjectAccessor(makeParser()).from({ key: null });
78
+ expect(a.get('key')).toBeNull();
79
+ });
80
+
81
+ it('handles array of objects at root level', () => {
82
+ const a = new ObjectAccessor(makeParser()).from({ list: [{ x: 1 }, { x: 2 }] });
83
+ expect(a.get('list')).toEqual([{ x: 1 }, { x: 2 }]);
84
+ });
85
+
86
+ it('handles nested array of arrays', () => {
87
+ const a = new ObjectAccessor(makeParser()).from({
88
+ matrix: [
89
+ [1, 2],
90
+ [3, 4],
91
+ ],
92
+ });
93
+ expect(a.get('matrix')).toEqual([
94
+ [1, 2],
95
+ [3, 4],
96
+ ]);
97
+ });
98
+
99
+ it('throws SecurityException at exactly maxDepth+1 nesting (strict=false so objectToRecord guard fires)', () => {
100
+ const secParser = new SecurityParser({ maxDepth: 2 });
101
+ const parser = makeParser(secParser);
102
+ expect(() =>
103
+ new ObjectAccessor(parser).strict(false).from({ a: { b: { c: { d: 1 } } } }),
104
+ ).toThrow(SecurityException);
105
+ });
106
+
107
+ it('does not throw at exactly maxDepth nesting (strict=false)', () => {
108
+ const secParser = new SecurityParser({ maxDepth: 2 });
109
+ const parser = makeParser(secParser);
110
+ expect(() =>
111
+ new ObjectAccessor(parser).strict(false).from({ a: { b: { c: 1 } } }),
112
+ ).not.toThrow();
113
+ });
114
+
115
+ it('throws SecurityException at exactly maxDepth+1 in nested array-of-objects (strict=false)', () => {
116
+ const secParser = new SecurityParser({ maxDepth: 1 });
117
+ const parser = makeParser(secParser);
118
+ expect(() => new ObjectAccessor(parser).strict(false).from({ items: [{ id: 1 }] })).toThrow(
119
+ SecurityException,
120
+ );
121
+ });
122
+
123
+ it('throws SecurityException when nested array of arrays exceeds depth (strict=false)', () => {
124
+ const secParser = new SecurityParser({ maxDepth: 0 });
125
+ const parser = makeParser(secParser);
126
+ expect(() => new ObjectAccessor(parser).strict(false).from({ matrix: [[1, 2]] })).toThrow(
127
+ SecurityException,
128
+ );
129
+ });
130
+
131
+ it('security exception message contains depth value', () => {
132
+ const secParser = new SecurityParser({ maxDepth: 0 });
133
+ const parser = makeParser(secParser);
134
+ expect(() => new ObjectAccessor(parser).from({ a: { b: 1 } })).toThrow(/depth/i);
135
+ });
136
+
137
+ it('does not throw convertArrayValues at exactly maxDepth (> not >=)', () => {
138
+ const secParser = new SecurityParser({ maxDepth: 1 });
139
+ const parser = makeParser(secParser);
140
+ expect(() =>
141
+ new ObjectAccessor(parser).strict(false).from({ items: [1, 2, 3] }),
142
+ ).not.toThrow();
143
+ });
144
+
145
+ it('handles null values inside an array correctly', () => {
146
+ const a = new ObjectAccessor(makeParser()).from({ items: [null, 1, 'text'] });
147
+ const items = a.get('items') as unknown[];
148
+ expect(items[0]).toBeNull();
149
+ expect(items[1]).toBe(1);
150
+ expect(items[2]).toBe('text');
151
+ });
152
+
153
+ it('handles array-of-arrays with primitives and does not throw at valid depth', () => {
154
+ const a = new ObjectAccessor(makeParser()).from({
155
+ matrix: [
156
+ [1, 2],
157
+ [3, 4],
158
+ ],
159
+ });
160
+ const matrix = a.get('matrix') as unknown[][];
161
+ expect(matrix[0]).toEqual([1, 2]);
162
+ expect(matrix[1]).toEqual([3, 4]);
163
+ });
164
+
165
+ it('throws when deeply nested arrays exceed maxDepth in convertArrayValues', () => {
166
+ const secParser = new SecurityParser({ maxDepth: 1 });
167
+ const parser = makeParser(secParser);
168
+ expect(() => new ObjectAccessor(parser).strict(false).from({ items: [[1, 2]] })).toThrow(
169
+ SecurityException,
170
+ );
171
+ });
172
+ });
@@ -0,0 +1,162 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { XmlAccessor } from '../../../src/accessors/formats/xml-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
+
9
+ function makeParser(secParser?: SecurityParser): DotNotationParser {
10
+ return new DotNotationParser(new SecurityGuard(), secParser ?? new SecurityParser());
11
+ }
12
+
13
+ describe(XmlAccessor.name, () => {
14
+ it('throws InvalidFormatException for non-string input', () => {
15
+ expect(() => new XmlAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
16
+ });
17
+
18
+ it('throws InvalidFormatException for number input', () => {
19
+ expect(() => new XmlAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
20
+ });
21
+
22
+ it('throws SecurityException for DOCTYPE declarations (XXE prevention)', () => {
23
+ const xml = '<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><root/>';
24
+ expect(() => new XmlAccessor(makeParser()).from(xml)).toThrow(SecurityException);
25
+ });
26
+
27
+ it('throws SecurityException for DOCTYPE regardless of case', () => {
28
+ const xml = '<!doctype foo><root/>';
29
+ expect(() => new XmlAccessor(makeParser()).from(xml)).toThrow(SecurityException);
30
+ });
31
+
32
+ it('parses a simple XML element', () => {
33
+ const a = new XmlAccessor(makeParser()).from('<root><name>Alice</name></root>');
34
+ expect(a.get('name')).toBe('Alice');
35
+ });
36
+
37
+ it('parses sibling elements under the root', () => {
38
+ const a = new XmlAccessor(makeParser()).from(
39
+ '<root><name>Alice</name><age>30</age></root>',
40
+ );
41
+ expect(a.get('name')).toBe('Alice');
42
+ expect(a.get('age')).toBe('30');
43
+ });
44
+
45
+ it('parses nested XML elements', () => {
46
+ const a = new XmlAccessor(makeParser()).from(
47
+ '<root><user><name>Alice</name></user></root>',
48
+ );
49
+ expect(a.get('user.name')).toBe('Alice');
50
+ });
51
+
52
+ it('throws InvalidFormatException for completely unparseable XML', () => {
53
+ expect(() => new XmlAccessor(makeParser()).from('not xml at all !@#')).toThrow(
54
+ InvalidFormatException,
55
+ );
56
+ });
57
+
58
+ it('returns empty object for self-closing root element', () => {
59
+ const a = new XmlAccessor(makeParser()).from('<root/>');
60
+ expect(a.all()).toEqual({});
61
+ });
62
+
63
+ it('merges duplicate sibling tags into an array', () => {
64
+ const a = new XmlAccessor(makeParser()).from('<root><item>a</item><item>b</item></root>');
65
+ const items = a.get('item');
66
+ expect(Array.isArray(items)).toBe(true);
67
+ expect((items as unknown[]).length).toBe(2);
68
+ });
69
+
70
+ it('parses with XML declaration header', () => {
71
+ const a = new XmlAccessor(makeParser()).from(
72
+ '<?xml version="1.0"?><root><key>value</key></root>',
73
+ );
74
+ expect(a.get('key')).toBe('value');
75
+ });
76
+
77
+ it('throws SecurityException when XML depth exceeds the limit', () => {
78
+ const secParser = new SecurityParser({ maxDepth: 1 });
79
+ const parser = makeParser(secParser);
80
+ const xml = '<root><a><b><c>deep</c></b></a></root>';
81
+ expect(() => new XmlAccessor(parser).from(xml)).toThrow(SecurityException);
82
+ });
83
+
84
+ it('parses deeply nested elements correctly', () => {
85
+ const a = new XmlAccessor(makeParser()).from(
86
+ '<root><level1><level2><value>deep</value></level2></level1></root>',
87
+ );
88
+ expect(a.get('level1.level2.value')).toBe('deep');
89
+ });
90
+
91
+ it('parses an element with plain text content', () => {
92
+ const a = new XmlAccessor(makeParser()).from('<root><text>hello world</text></root>');
93
+ expect(a.get('text')).toBe('hello world');
94
+ });
95
+
96
+ it('merges three duplicate sibling tags into an array of 3', () => {
97
+ const a = new XmlAccessor(makeParser()).from(
98
+ '<root><item>a</item><item>b</item><item>c</item></root>',
99
+ );
100
+ const items = a.get('item') as unknown[];
101
+ expect(Array.isArray(items)).toBe(true);
102
+ expect(items.length).toBe(3);
103
+ expect(items[2]).toBe('c');
104
+ });
105
+
106
+ it('parses self-closing child element as empty string text', () => {
107
+ const a = new XmlAccessor(makeParser()).from('<root><empty/><name>Alice</name></root>');
108
+ expect(a.get('name')).toBe('Alice');
109
+ });
110
+
111
+ it('parses multiple different child elements', () => {
112
+ const a = new XmlAccessor(makeParser()).from(
113
+ '<root><first>1</first><second>2</second><third>3</third></root>',
114
+ );
115
+ expect(a.get('first')).toBe('1');
116
+ expect(a.get('second')).toBe('2');
117
+ expect(a.get('third')).toBe('3');
118
+ });
119
+
120
+ it('returns empty object for root with only whitespace content', () => {
121
+ const a = new XmlAccessor(makeParser()).from('<root> </root>');
122
+ expect(a.all()).toEqual({});
123
+ });
124
+
125
+ it('returns nested structure for complex XML', () => {
126
+ const a = new XmlAccessor(makeParser()).from(
127
+ '<root><user><name>Alice</name><role>admin</role></user></root>',
128
+ );
129
+ expect(a.get('user.name')).toBe('Alice');
130
+ expect(a.get('user.role')).toBe('admin');
131
+ });
132
+
133
+ it('returns plain string value when child has only text content', () => {
134
+ const a = new XmlAccessor(makeParser()).from('<root><title>Hello World</title></root>');
135
+ expect(typeof a.get('title')).toBe('string');
136
+ expect(a.get('title')).toBe('Hello World');
137
+ });
138
+
139
+ it('builds an array when the same tag appears 4 times', () => {
140
+ const a = new XmlAccessor(makeParser()).from(
141
+ '<root><k>1</k><k>2</k><k>3</k><k>4</k></root>',
142
+ );
143
+ const k = a.get('k') as unknown[];
144
+ expect(Array.isArray(k)).toBe(true);
145
+ expect(k.length).toBe(4);
146
+ expect(k[3]).toBe('4');
147
+ });
148
+
149
+ it('throws SecurityException when opening-tag count exceeds SecurityParser.maxKeys (getMaxKeys flows to XmlParser.maxElements)', () => {
150
+ const secParser = new SecurityParser({ maxKeys: 2 });
151
+ const parser = makeParser(secParser);
152
+ const xml = '<root>' + '<item>x</item>'.repeat(3) + '</root>';
153
+ expect(() => new XmlAccessor(parser).from(xml)).toThrow(SecurityException);
154
+ });
155
+
156
+ it('does not throw when opening-tag count is within SecurityParser.maxKeys', () => {
157
+ const secParser = new SecurityParser({ maxKeys: 10 });
158
+ const parser = makeParser(secParser);
159
+ const xml = '<root>' + '<item>x</item>'.repeat(3) + '</root>';
160
+ expect(() => new XmlAccessor(parser).from(xml)).not.toThrow();
161
+ });
162
+ });
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { YamlAccessor } from '../../../src/accessors/formats/yaml-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(YamlAccessor.name, () => {
13
+ it('parses a valid YAML string', () => {
14
+ const a = new YamlAccessor(makeParser()).from('name: Alice\nage: 30');
15
+ expect(a.get('name')).toBe('Alice');
16
+ expect(a.get('age')).toBe(30);
17
+ });
18
+
19
+ it('throws InvalidFormatException for non-string input', () => {
20
+ expect(() => new YamlAccessor(makeParser()).from(null)).toThrow(InvalidFormatException);
21
+ });
22
+
23
+ it('throws InvalidFormatException for number input', () => {
24
+ expect(() => new YamlAccessor(makeParser()).from(42)).toThrow(InvalidFormatException);
25
+ });
26
+
27
+ it('resolves a nested path', () => {
28
+ const a = new YamlAccessor(makeParser()).from('user:\n name: Bob');
29
+ expect(a.get('user.name')).toBe('Bob');
30
+ });
31
+
32
+ it('returns null for a missing path', () => {
33
+ const a = new YamlAccessor(makeParser()).from('key: value');
34
+ expect(a.get('missing')).toBeNull();
35
+ });
36
+ });