ts-data-forge 1.0.0

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 (143) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +534 -0
  3. package/package.json +101 -0
  4. package/src/array/array-utils-creation.test.mts +443 -0
  5. package/src/array/array-utils-modification.test.mts +197 -0
  6. package/src/array/array-utils-overload-type-error.test.mts +149 -0
  7. package/src/array/array-utils-reducing-value.test.mts +425 -0
  8. package/src/array/array-utils-search.test.mts +169 -0
  9. package/src/array/array-utils-set-op.test.mts +335 -0
  10. package/src/array/array-utils-slice-clamped.test.mts +113 -0
  11. package/src/array/array-utils-slicing.test.mts +316 -0
  12. package/src/array/array-utils-transformation.test.mts +790 -0
  13. package/src/array/array-utils-validation.test.mts +492 -0
  14. package/src/array/array-utils.mts +4000 -0
  15. package/src/array/array.test.mts +146 -0
  16. package/src/array/index.mts +2 -0
  17. package/src/array/tuple-utils.mts +519 -0
  18. package/src/array/tuple-utils.test.mts +518 -0
  19. package/src/collections/imap-mapped.mts +801 -0
  20. package/src/collections/imap-mapped.test.mts +860 -0
  21. package/src/collections/imap.mts +651 -0
  22. package/src/collections/imap.test.mts +932 -0
  23. package/src/collections/index.mts +6 -0
  24. package/src/collections/iset-mapped.mts +889 -0
  25. package/src/collections/iset-mapped.test.mts +1187 -0
  26. package/src/collections/iset.mts +682 -0
  27. package/src/collections/iset.test.mts +1084 -0
  28. package/src/collections/queue.mts +390 -0
  29. package/src/collections/queue.test.mts +282 -0
  30. package/src/collections/stack.mts +423 -0
  31. package/src/collections/stack.test.mts +225 -0
  32. package/src/expect-type.mts +206 -0
  33. package/src/functional/index.mts +4 -0
  34. package/src/functional/match.mts +300 -0
  35. package/src/functional/match.test.mts +177 -0
  36. package/src/functional/optional.mts +733 -0
  37. package/src/functional/optional.test.mts +619 -0
  38. package/src/functional/pipe.mts +212 -0
  39. package/src/functional/pipe.test.mts +85 -0
  40. package/src/functional/result.mts +1134 -0
  41. package/src/functional/result.test.mts +777 -0
  42. package/src/globals.d.mts +38 -0
  43. package/src/guard/has-key.mts +119 -0
  44. package/src/guard/has-key.test.mts +219 -0
  45. package/src/guard/index.mts +7 -0
  46. package/src/guard/is-non-empty-string.mts +108 -0
  47. package/src/guard/is-non-empty-string.test.mts +91 -0
  48. package/src/guard/is-non-null-object.mts +106 -0
  49. package/src/guard/is-non-null-object.test.mts +90 -0
  50. package/src/guard/is-primitive.mts +165 -0
  51. package/src/guard/is-primitive.test.mts +102 -0
  52. package/src/guard/is-record.mts +153 -0
  53. package/src/guard/is-record.test.mts +112 -0
  54. package/src/guard/is-type.mts +450 -0
  55. package/src/guard/is-type.test.mts +496 -0
  56. package/src/guard/key-is-in.mts +163 -0
  57. package/src/guard/key-is-in.test.mts +19 -0
  58. package/src/index.mts +10 -0
  59. package/src/iterator/index.mts +1 -0
  60. package/src/iterator/range.mts +120 -0
  61. package/src/iterator/range.test.mts +33 -0
  62. package/src/json/index.mts +1 -0
  63. package/src/json/json.mts +711 -0
  64. package/src/json/json.test.mts +628 -0
  65. package/src/number/branded-types/finite-number.mts +354 -0
  66. package/src/number/branded-types/finite-number.test.mts +135 -0
  67. package/src/number/branded-types/index.mts +26 -0
  68. package/src/number/branded-types/int.mts +278 -0
  69. package/src/number/branded-types/int.test.mts +140 -0
  70. package/src/number/branded-types/int16.mts +192 -0
  71. package/src/number/branded-types/int16.test.mts +170 -0
  72. package/src/number/branded-types/int32.mts +193 -0
  73. package/src/number/branded-types/int32.test.mts +170 -0
  74. package/src/number/branded-types/non-negative-finite-number.mts +223 -0
  75. package/src/number/branded-types/non-negative-finite-number.test.mts +188 -0
  76. package/src/number/branded-types/non-negative-int16.mts +187 -0
  77. package/src/number/branded-types/non-negative-int16.test.mts +201 -0
  78. package/src/number/branded-types/non-negative-int32.mts +187 -0
  79. package/src/number/branded-types/non-negative-int32.test.mts +204 -0
  80. package/src/number/branded-types/non-zero-finite-number.mts +229 -0
  81. package/src/number/branded-types/non-zero-finite-number.test.mts +198 -0
  82. package/src/number/branded-types/non-zero-int.mts +167 -0
  83. package/src/number/branded-types/non-zero-int.test.mts +177 -0
  84. package/src/number/branded-types/non-zero-int16.mts +196 -0
  85. package/src/number/branded-types/non-zero-int16.test.mts +195 -0
  86. package/src/number/branded-types/non-zero-int32.mts +196 -0
  87. package/src/number/branded-types/non-zero-int32.test.mts +197 -0
  88. package/src/number/branded-types/non-zero-safe-int.mts +196 -0
  89. package/src/number/branded-types/non-zero-safe-int.test.mts +232 -0
  90. package/src/number/branded-types/non-zero-uint16.mts +189 -0
  91. package/src/number/branded-types/non-zero-uint16.test.mts +199 -0
  92. package/src/number/branded-types/non-zero-uint32.mts +189 -0
  93. package/src/number/branded-types/non-zero-uint32.test.mts +199 -0
  94. package/src/number/branded-types/positive-finite-number.mts +241 -0
  95. package/src/number/branded-types/positive-finite-number.test.mts +204 -0
  96. package/src/number/branded-types/positive-int.mts +304 -0
  97. package/src/number/branded-types/positive-int.test.mts +176 -0
  98. package/src/number/branded-types/positive-int16.mts +188 -0
  99. package/src/number/branded-types/positive-int16.test.mts +197 -0
  100. package/src/number/branded-types/positive-int32.mts +188 -0
  101. package/src/number/branded-types/positive-int32.test.mts +197 -0
  102. package/src/number/branded-types/positive-safe-int.mts +187 -0
  103. package/src/number/branded-types/positive-safe-int.test.mts +210 -0
  104. package/src/number/branded-types/positive-uint16.mts +188 -0
  105. package/src/number/branded-types/positive-uint16.test.mts +203 -0
  106. package/src/number/branded-types/positive-uint32.mts +188 -0
  107. package/src/number/branded-types/positive-uint32.test.mts +203 -0
  108. package/src/number/branded-types/safe-int.mts +291 -0
  109. package/src/number/branded-types/safe-int.test.mts +170 -0
  110. package/src/number/branded-types/safe-uint.mts +187 -0
  111. package/src/number/branded-types/safe-uint.test.mts +176 -0
  112. package/src/number/branded-types/uint.mts +179 -0
  113. package/src/number/branded-types/uint.test.mts +158 -0
  114. package/src/number/branded-types/uint16.mts +186 -0
  115. package/src/number/branded-types/uint16.test.mts +170 -0
  116. package/src/number/branded-types/uint32.mts +218 -0
  117. package/src/number/branded-types/uint32.test.mts +170 -0
  118. package/src/number/enum/index.mts +2 -0
  119. package/src/number/enum/int8.mts +344 -0
  120. package/src/number/enum/int8.test.mts +180 -0
  121. package/src/number/enum/uint8.mts +293 -0
  122. package/src/number/enum/uint8.test.mts +164 -0
  123. package/src/number/index.mts +4 -0
  124. package/src/number/num.mts +604 -0
  125. package/src/number/num.test.mts +242 -0
  126. package/src/number/refined-number-utils.mts +566 -0
  127. package/src/object/index.mts +1 -0
  128. package/src/object/object.mts +447 -0
  129. package/src/object/object.test.mts +124 -0
  130. package/src/others/cast-mutable.mts +113 -0
  131. package/src/others/cast-readonly.mts +192 -0
  132. package/src/others/cast-readonly.test.mts +89 -0
  133. package/src/others/if-then.mts +98 -0
  134. package/src/others/if-then.test.mts +75 -0
  135. package/src/others/index.mts +7 -0
  136. package/src/others/map-nullable.mts +172 -0
  137. package/src/others/map-nullable.test.mts +297 -0
  138. package/src/others/memoize-function.mts +196 -0
  139. package/src/others/memoize-function.test.mts +168 -0
  140. package/src/others/tuple.mts +160 -0
  141. package/src/others/tuple.test.mts +11 -0
  142. package/src/others/unknown-to-string.mts +215 -0
  143. package/src/others/unknown-to-string.test.mts +114 -0
@@ -0,0 +1,628 @@
1
+ import { Arr } from '../array/index.mjs';
2
+ import { Result } from '../functional/result.mjs';
3
+ import { hasKey, isRecord } from '../guard/index.mjs';
4
+ import { Json } from './json.mjs';
5
+
6
+ describe('parse', () => {
7
+ test('should parse primitive values', () => {
8
+ expect(Json.parse('"hello"')).toStrictEqual(Result.ok('hello'));
9
+ expect(Json.parse('42')).toStrictEqual(Result.ok(42));
10
+ expect(Json.parse('true')).toStrictEqual(Result.ok(true));
11
+ expect(Json.parse('false')).toStrictEqual(Result.ok(false));
12
+ expect(Json.parse('null')).toStrictEqual(Result.ok(null));
13
+ });
14
+
15
+ test('should parse arrays', () => {
16
+ expect(Json.parse('[1,2,3]')).toStrictEqual(Result.ok([1, 2, 3]));
17
+ expect(Json.parse('["a","b","c"]')).toStrictEqual(
18
+ Result.ok(['a', 'b', 'c']),
19
+ );
20
+ expect(Json.parse('[1,"two",true,null]')).toStrictEqual(
21
+ Result.ok([1, 'two', true, null]),
22
+ );
23
+ });
24
+
25
+ test('should parse objects', () => {
26
+ expect(Json.parse('{"a":1,"b":2}')).toStrictEqual(
27
+ Result.ok({ a: 1, b: 2 }),
28
+ );
29
+ expect(Json.parse('{"name":"test","value":42}')).toStrictEqual(
30
+ Result.ok({
31
+ name: 'test',
32
+ value: 42,
33
+ }),
34
+ );
35
+ });
36
+
37
+ test('should parse nested structures', () => {
38
+ const json = '{"level1":{"level2":{"array":[1,2,{"level3":"deep"}]}}}';
39
+ const expected = {
40
+ level1: {
41
+ level2: {
42
+ array: [1, 2, { level3: 'deep' }],
43
+ },
44
+ },
45
+ };
46
+ expect(Json.parse(json)).toStrictEqual(Result.ok(expected));
47
+ });
48
+
49
+ test('should handle whitespace', () => {
50
+ expect(Json.parse(' { "a" : 1 , "b" : 2 } ')).toStrictEqual(
51
+ Result.ok({ a: 1, b: 2 }),
52
+ );
53
+ expect(Json.parse('\n[\n 1,\n 2,\n 3\n]\n')).toStrictEqual(
54
+ Result.ok([1, 2, 3]),
55
+ );
56
+ });
57
+
58
+ test('should return error for invalid JSON', () => {
59
+ expect(Result.isErr(Json.parse('invalid'))).toBe(true);
60
+ expect(Result.isErr(Json.parse('{missing quotes: true}'))).toBe(true);
61
+ expect(Result.isErr(Json.parse('[1,2,]'))).toBe(true); // Trailing comma
62
+ expect(Result.isErr(Json.parse('undefined'))).toBe(true);
63
+ });
64
+
65
+ test('should return parsed value for valid JSON', () => {
66
+ expect(Json.parse('{"a":1}')).toStrictEqual(Result.ok({ a: 1 }));
67
+ expect(Json.parse('[1,2,3]')).toStrictEqual(Result.ok([1, 2, 3]));
68
+ expect(Json.parse('"string"')).toStrictEqual(Result.ok('string'));
69
+ expect(Json.parse('42')).toStrictEqual(Result.ok(42));
70
+ expect(Json.parse('true')).toStrictEqual(Result.ok(true));
71
+ expect(Json.parse('null')).toStrictEqual(Result.ok(null));
72
+ });
73
+
74
+ test('should return error for invalid JSON cases', () => {
75
+ expect(Result.isErr(Json.parse('invalid'))).toBe(true);
76
+ expect(Result.isErr(Json.parse('{bad json}'))).toBe(true);
77
+ expect(Result.isErr(Json.parse('[1,2,]'))).toBe(true);
78
+ expect(Result.isErr(Json.parse('undefined'))).toBe(true);
79
+ expect(Result.isErr(Json.parse(''))).toBe(true);
80
+ });
81
+
82
+ test('should handle edge cases', () => {
83
+ expect(Json.parse('0')).toStrictEqual(Result.ok(0));
84
+ expect(Json.parse('""')).toStrictEqual(Result.ok(''));
85
+ expect(Json.parse('[]')).toStrictEqual(Result.ok([]));
86
+ expect(Json.parse('{}')).toStrictEqual(Result.ok({}));
87
+ });
88
+
89
+ test('should not throw errors', () => {
90
+ expect(() => Json.parse('{{{')).not.toThrow();
91
+ expect(() => Json.parse('null null')).not.toThrow();
92
+
93
+ expect(() => Json.parse(String(undefined))).not.toThrow();
94
+ });
95
+
96
+ test('should use reviver function to transform values', () => {
97
+ const dateReviver = (_key: string, value: unknown): unknown => {
98
+ if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/u.test(value)) {
99
+ return new Date(value);
100
+ }
101
+ return value;
102
+ };
103
+
104
+ const jsonString = '{"name":"test","created":"2023-12-01T10:00:00.000Z"}';
105
+ const result = Json.parse(jsonString, dateReviver);
106
+
107
+ expect(Result.isOk(result)).toBe(true);
108
+ if (Result.isOk(result)) {
109
+ if (
110
+ isRecord(result.value) &&
111
+ hasKey(result.value, 'name') &&
112
+ hasKey(result.value, 'created')
113
+ ) {
114
+ expect(result.value.name).toBe('test');
115
+ expect(result.value.created).toBeInstanceOf(Date);
116
+ }
117
+ }
118
+ });
119
+
120
+ test('should handle reviver returning different types', () => {
121
+ const transformReviver = (key: string, value: unknown): unknown => {
122
+ if (key === 'number' && typeof value === 'string') {
123
+ return Number.parseInt(value, 10);
124
+ }
125
+ return value;
126
+ };
127
+
128
+ const result = Json.parse(
129
+ '{"number":"42","text":"hello"}',
130
+ transformReviver,
131
+ );
132
+
133
+ expect(Result.isOk(result)).toBe(true);
134
+ if (Result.isOk(result)) {
135
+ expect(result.value).toHaveProperty('number');
136
+ expect(result.value).toHaveProperty('text');
137
+ if (
138
+ isRecord(result.value) &&
139
+ hasKey(result.value, 'number') &&
140
+ hasKey(result.value, 'text')
141
+ ) {
142
+ expect(result.value.number).toBe(42);
143
+ expect(result.value.text).toBe('hello');
144
+ }
145
+ }
146
+ });
147
+ });
148
+
149
+ describe('stringify', () => {
150
+ test('should stringify primitive values', () => {
151
+ expect(Json.stringify('hello')).toStrictEqual(Result.ok('"hello"'));
152
+ expect(Json.stringify(42)).toStrictEqual(Result.ok('42'));
153
+ expect(Json.stringify(true)).toStrictEqual(Result.ok('true'));
154
+ expect(Json.stringify(null)).toStrictEqual(Result.ok('null'));
155
+ });
156
+
157
+ test('should stringify arrays', () => {
158
+ expect(Json.stringify([1, 2, 3])).toStrictEqual(Result.ok('[1,2,3]'));
159
+ expect(Json.stringify(['a', 'b', 'c'])).toStrictEqual(
160
+ Result.ok('["a","b","c"]'),
161
+ );
162
+ expect(Json.stringify([1, 'two', true, null])).toStrictEqual(
163
+ Result.ok('[1,"two",true,null]'),
164
+ );
165
+ });
166
+
167
+ test('should stringify objects', () => {
168
+ expect(Json.stringify({ a: 1, b: 2 })).toStrictEqual(
169
+ Result.ok('{"a":1,"b":2}'),
170
+ );
171
+ expect(Json.stringify({ name: 'test', value: 42 })).toStrictEqual(
172
+ Result.ok('{"name":"test","value":42}'),
173
+ );
174
+ });
175
+
176
+ test('should stringify nested structures', () => {
177
+ const nested = {
178
+ level1: {
179
+ level2: {
180
+ array: [1, 2, { level3: 'deep' }],
181
+ },
182
+ },
183
+ };
184
+ expect(Json.stringify(nested)).toStrictEqual(
185
+ Result.ok('{"level1":{"level2":{"array":[1,2,{"level3":"deep"}]}}}'),
186
+ );
187
+ });
188
+
189
+ test('should handle empty structures', () => {
190
+ expect(Json.stringify({})).toStrictEqual(Result.ok('{}'));
191
+ expect(Json.stringify([])).toStrictEqual(Result.ok('[]'));
192
+ });
193
+
194
+ test('should handle special string values', () => {
195
+ expect(Json.stringify('with "quotes"')).toStrictEqual(
196
+ Result.ok('"with \\"quotes\\""'),
197
+ );
198
+ expect(Json.stringify('with\nnewline')).toStrictEqual(
199
+ Result.ok('"with\\nnewline"'),
200
+ );
201
+ expect(Json.stringify('with\ttab')).toStrictEqual(
202
+ Result.ok('"with\\ttab"'),
203
+ );
204
+ });
205
+
206
+ test('should return stringified value for valid JSON values', () => {
207
+ expect(Json.stringify({ a: 1 })).toStrictEqual(Result.ok('{"a":1}'));
208
+ expect(Json.stringify([1, 2, 3])).toStrictEqual(Result.ok('[1,2,3]'));
209
+ expect(Json.stringify('string')).toStrictEqual(Result.ok('"string"'));
210
+ expect(Json.stringify(42)).toStrictEqual(Result.ok('42'));
211
+ expect(Json.stringify(true)).toStrictEqual(Result.ok('true'));
212
+ expect(Json.stringify(null)).toStrictEqual(Result.ok('null'));
213
+ });
214
+
215
+ test('should handle non-serializable values', () => {
216
+ expect(Json.stringify(undefined)).toStrictEqual(Result.ok(undefined));
217
+ expect(Json.stringify(Symbol('test'))).toStrictEqual(Result.ok(undefined));
218
+ expect(Json.stringify(() => {})).toStrictEqual(Result.ok(undefined));
219
+ // BigInt should cause an error
220
+ expect(Result.isErr(Json.stringify(BigInt(123)))).toBe(true);
221
+ });
222
+
223
+ test('should handle circular references', () => {
224
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
225
+ const obj: any = { a: 1 };
226
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
227
+ obj.circular = obj;
228
+ expect(Result.isErr(Json.stringify(obj))).toBe(true);
229
+ });
230
+
231
+ test('should handle objects with toJSON method', () => {
232
+ const obj = {
233
+ toJSON() {
234
+ return { custom: 'value' };
235
+ },
236
+ };
237
+ expect(Json.stringify(obj)).toStrictEqual(Result.ok('{"custom":"value"}'));
238
+ });
239
+
240
+ test('should handle Date objects', () => {
241
+ const date = new Date('2023-01-01T00:00:00.000Z');
242
+ expect(Json.stringify(date)).toStrictEqual(
243
+ Result.ok('"2023-01-01T00:00:00.000Z"'),
244
+ );
245
+ });
246
+
247
+ test('should not throw errors', () => {
248
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
249
+ const circularArray: any[] = [];
250
+ circularArray.push(circularArray);
251
+
252
+ expect(() => Json.stringify(circularArray)).not.toThrow();
253
+ expect(() => Json.stringify({ fn: () => {} })).not.toThrow();
254
+ });
255
+
256
+ test('should use replacer function to filter values', () => {
257
+ const data = {
258
+ name: 'John',
259
+ password: 'secret123',
260
+ email: 'john@example.com',
261
+ };
262
+
263
+ const secureReplacer = (key: string, value: unknown): unknown => {
264
+ if (key === 'password') return '[REDACTED]';
265
+ return value;
266
+ };
267
+
268
+ const result = Json.stringify(data, secureReplacer);
269
+
270
+ expect(Result.isOk(result)).toBe(true);
271
+ if (Result.isOk(result)) {
272
+ expect(result.value).toContain('[REDACTED]');
273
+ expect(result.value).not.toContain('secret123');
274
+ }
275
+ });
276
+
277
+ test('should format output with space parameter (number)', () => {
278
+ const data = { a: 1, b: 2 };
279
+ const result = Json.stringify(data, undefined, 2);
280
+
281
+ expect(Result.isOk(result)).toBe(true);
282
+ if (Result.isOk(result)) {
283
+ expect(result.value).toContain('\n');
284
+ expect(result.value).toContain(' '); // 2 spaces indentation
285
+ }
286
+ });
287
+
288
+ test('should format output with space parameter (string)', () => {
289
+ const data = { a: 1, b: 2 };
290
+ const result = Json.stringify(data, undefined, '\t');
291
+
292
+ expect(Result.isOk(result)).toBe(true);
293
+ if (Result.isOk(result)) {
294
+ expect(result.value).toContain('\n');
295
+ expect(result.value).toContain('\t'); // tab indentation
296
+ }
297
+ });
298
+ });
299
+
300
+ describe('stringifySelected', () => {
301
+ test('should include only selected properties', () => {
302
+ const user = {
303
+ id: 1,
304
+ name: 'Alice',
305
+ email: 'alice@example.com',
306
+ password: 'secret123',
307
+ lastLogin: '2023-12-01',
308
+ };
309
+
310
+ const result = Json.stringifySelected(user, ['id', 'name', 'email']);
311
+
312
+ expect(Result.isOk(result)).toBe(true);
313
+ if (Result.isOk(result)) {
314
+ const parsed: unknown = JSON.parse(result.value);
315
+ expect(parsed).toStrictEqual({
316
+ id: 1,
317
+ name: 'Alice',
318
+ email: 'alice@example.com',
319
+ });
320
+ expect(parsed).not.toHaveProperty('password');
321
+ expect(parsed).not.toHaveProperty('lastLogin');
322
+ }
323
+ });
324
+
325
+ test('should work with nested objects', () => {
326
+ const data = {
327
+ users: [
328
+ { id: 1, name: 'Alice', secret: 'hidden1' },
329
+ { id: 2, name: 'Bob', secret: 'hidden2' },
330
+ ],
331
+ metadata: { total: 2, page: 1, internal: 'secret' },
332
+ };
333
+
334
+ const result = Json.stringifySelected(data, [
335
+ 'users',
336
+ 'id',
337
+ 'name',
338
+ 'metadata',
339
+ 'total',
340
+ ]);
341
+
342
+ expect(Result.isOk(result)).toBe(true);
343
+ if (Result.isOk(result)) {
344
+ const parsed: unknown = JSON.parse(result.value);
345
+ if (isRecord(parsed) && hasKey(parsed, 'users')) {
346
+ expect(isRecord(parsed.users)).toBe(false);
347
+ expect(parsed.users).toHaveLength(2);
348
+ if (Arr.isArray(parsed.users)) {
349
+ expect(parsed.users[0]).toStrictEqual({ id: 1, name: 'Alice' });
350
+ expect(parsed.users[0]).not.toHaveProperty('secret');
351
+ }
352
+ if (isRecord(parsed) && hasKey(parsed, 'metadata')) {
353
+ expect(parsed.metadata).toStrictEqual({ total: 2 });
354
+ expect(parsed.metadata).not.toHaveProperty('page');
355
+ expect(parsed.metadata).not.toHaveProperty('internal');
356
+ }
357
+ }
358
+ }
359
+ });
360
+
361
+ test('should work with array indices', () => {
362
+ const matrix = [
363
+ [1, 2, 3, 4],
364
+ [5, 6, 7, 8],
365
+ [9, 10, 11, 12],
366
+ ];
367
+
368
+ const result = Json.stringifySelected(matrix, [0, 1]);
369
+
370
+ expect(Result.isOk(result)).toBe(true);
371
+ if (Result.isOk(result)) {
372
+ const parsed: unknown = JSON.parse(result.value);
373
+ // Note: stringifySelected works with JSON.stringify's replacer parameter
374
+ // which may not work as expected with arrays
375
+ expect(Array.isArray(parsed)).toBe(true);
376
+ expect(parsed).toHaveLength(3);
377
+ }
378
+ });
379
+
380
+ test('should handle formatting with space parameter', () => {
381
+ const data = { a: 1, b: { c: 2 } };
382
+ const result = Json.stringifySelected(data, ['a', 'b', 'c'], 2);
383
+
384
+ expect(Result.isOk(result)).toBe(true);
385
+ if (Result.isOk(result)) {
386
+ expect(result.value).toContain('\n');
387
+ expect(result.value).toContain(' ');
388
+ }
389
+ });
390
+
391
+ test('should handle empty selection array', () => {
392
+ const data = { a: 1, b: 2, c: 3 };
393
+ const result = Json.stringifySelected(data, []);
394
+
395
+ expect(Result.isOk(result)).toBe(true);
396
+ if (Result.isOk(result)) {
397
+ expect(result.value).toBe('{}');
398
+ }
399
+ });
400
+
401
+ test('should handle undefined properties parameter', () => {
402
+ const data = { a: 1, b: 2 };
403
+ const result = Json.stringifySelected(data, undefined);
404
+
405
+ expect(Result.isOk(result)).toBe(true);
406
+ if (Result.isOk(result)) {
407
+ const parsed: unknown = JSON.parse(result.value);
408
+ expect(parsed).toStrictEqual({ a: 1, b: 2 });
409
+ }
410
+ });
411
+
412
+ test('should handle circular references with error', () => {
413
+ type CircularType = { name: string; self?: CircularType };
414
+ const circular: CircularType = { name: 'test' };
415
+ circular.self = circular;
416
+
417
+ const result = Json.stringifySelected(circular, ['name', 'self']);
418
+
419
+ // Note: JSON.stringify may handle circular references differently depending on the replacer
420
+ expect(Result.isOk(result) || Result.isErr(result)).toBe(true);
421
+ if (Result.isErr(result)) {
422
+ expect(typeof result.value).toBe('string');
423
+ }
424
+ });
425
+ });
426
+
427
+ describe('stringifySortedKey', () => {
428
+ test('should sort object keys alphabetically', () => {
429
+ const unsortedObj = {
430
+ zebra: 'animal',
431
+ apple: 'fruit',
432
+ banana: 'fruit',
433
+ aardvark: 'animal',
434
+ };
435
+
436
+ const result = Json.stringifySortedKey(unsortedObj);
437
+
438
+ expect(Result.isOk(result)).toBe(true);
439
+ if (Result.isOk(result)) {
440
+ expect(result.value).toBe(
441
+ '{"aardvark":"animal","apple":"fruit","banana":"fruit","zebra":"animal"}',
442
+ );
443
+ }
444
+ });
445
+
446
+ test('should sort nested object keys', () => {
447
+ const nestedObj = {
448
+ user: {
449
+ name: 'Alice',
450
+ age: 30,
451
+ address: {
452
+ zip: '12345',
453
+ city: 'New York',
454
+ country: 'USA',
455
+ },
456
+ },
457
+ settings: {
458
+ theme: 'dark',
459
+ language: 'en',
460
+ },
461
+ };
462
+
463
+ const result = Json.stringifySortedKey(nestedObj);
464
+
465
+ expect(Result.isOk(result)).toBe(true);
466
+ if (Result.isOk(result)) {
467
+ const parsed: unknown = JSON.parse(result.value);
468
+ if (isRecord(parsed)) {
469
+ const keys = Object.keys(parsed);
470
+ expect(keys).toStrictEqual(['settings', 'user']); // sorted top-level keys
471
+
472
+ if (hasKey(parsed, 'user') && isRecord(parsed.user)) {
473
+ const userKeys = Object.keys(parsed.user);
474
+ expect(userKeys).toStrictEqual(['address', 'age', 'name']); // sorted nested keys
475
+
476
+ if (hasKey(parsed.user, 'address') && isRecord(parsed.user.address)) {
477
+ const addressKeys = Object.keys(parsed.user.address);
478
+ expect(addressKeys).toStrictEqual(['city', 'country', 'zip']); // sorted deeper nested keys
479
+ }
480
+ }
481
+ }
482
+ }
483
+ });
484
+
485
+ test('should handle arrays with objects', () => {
486
+ const dataWithArrays = {
487
+ users: [
488
+ { name: 'Bob', id: 2, active: true },
489
+ { name: 'Alice', id: 1, active: false },
490
+ ],
491
+ metadata: {
492
+ version: '1.0',
493
+ created: '2023-12-01',
494
+ author: 'system',
495
+ },
496
+ };
497
+
498
+ const result = Json.stringifySortedKey(dataWithArrays);
499
+
500
+ expect(Result.isOk(result)).toBe(true);
501
+ if (Result.isOk(result)) {
502
+ const parsed: unknown = JSON.parse(result.value);
503
+
504
+ if (isRecord(parsed)) {
505
+ // Check top-level keys are sorted
506
+ const topKeys = Object.keys(parsed);
507
+ expect(topKeys).toStrictEqual(['metadata', 'users']);
508
+
509
+ // Check metadata keys are sorted
510
+ if (hasKey(parsed, 'metadata') && isRecord(parsed.metadata)) {
511
+ const metadataKeys = Object.keys(parsed.metadata);
512
+ expect(metadataKeys).toStrictEqual(['author', 'created', 'version']);
513
+ }
514
+
515
+ // Check user object keys are sorted
516
+ if (
517
+ hasKey(parsed, 'users') &&
518
+ Arr.isArray(parsed.users) &&
519
+ Arr.isNonEmpty(parsed.users)
520
+ ) {
521
+ const firstUser = parsed.users[0];
522
+ if (isRecord(firstUser)) {
523
+ const userKeys = Object.keys(firstUser);
524
+ expect(userKeys).toStrictEqual(['active', 'id', 'name']);
525
+ }
526
+ }
527
+ }
528
+ }
529
+ });
530
+
531
+ test('should handle formatting with space parameter', () => {
532
+ const obj = { b: 2, a: 1 };
533
+ const result = Json.stringifySortedKey(obj, 2);
534
+
535
+ expect(Result.isOk(result)).toBe(true);
536
+ if (Result.isOk(result)) {
537
+ expect(result.value).toContain('\n');
538
+ expect(result.value).toContain(' ');
539
+ expect(result.value).toMatch(/\{\s+"a": 1,\s+"b": 2\s+\}/u);
540
+ }
541
+ });
542
+
543
+ test('should produce deterministic output', () => {
544
+ const obj1 = { c: 3, a: 1, b: 2 };
545
+ const obj2 = { b: 2, a: 1, c: 3 };
546
+
547
+ const result1 = Json.stringifySortedKey(obj1);
548
+ const result2 = Json.stringifySortedKey(obj2);
549
+
550
+ expect(Result.isOk(result1)).toBe(true);
551
+ expect(Result.isOk(result2)).toBe(true);
552
+
553
+ if (Result.isOk(result1) && Result.isOk(result2)) {
554
+ expect(result1.value).toBe(result2.value);
555
+ }
556
+ });
557
+
558
+ test('should handle problematic objects', () => {
559
+ try {
560
+ type CircularObj = {
561
+ normal: string;
562
+ circular: { self?: CircularObj };
563
+ };
564
+ const problematicObj: CircularObj = {
565
+ normal: 'value',
566
+ circular: {},
567
+ };
568
+ problematicObj.circular.self = problematicObj;
569
+
570
+ const result = Json.stringifySortedKey(problematicObj);
571
+
572
+ // This may throw due to circular reference during key extraction
573
+ expect(Result.isErr(result)).toBe(true);
574
+ if (Result.isErr(result)) {
575
+ expect(typeof result.value).toBe('string');
576
+ }
577
+ } catch (error) {
578
+ // Expected if circular reference causes stack overflow
579
+ expect(error).toBeDefined();
580
+ }
581
+ });
582
+
583
+ test('should handle empty object', () => {
584
+ const result = Json.stringifySortedKey({});
585
+
586
+ expect(Result.isOk(result)).toBe(true);
587
+ if (Result.isOk(result)) {
588
+ expect(result.value).toBe('{}');
589
+ }
590
+ });
591
+
592
+ test('should handle deeply nested structures', () => {
593
+ const deep = {
594
+ level1: {
595
+ z: 'last',
596
+ a: {
597
+ nested: {
598
+ y: 2,
599
+ x: 1,
600
+ },
601
+ },
602
+ },
603
+ };
604
+
605
+ const result = Json.stringifySortedKey(deep);
606
+
607
+ expect(Result.isOk(result)).toBe(true);
608
+ if (Result.isOk(result)) {
609
+ const parsed: unknown = JSON.parse(result.value);
610
+ if (isRecord(parsed) && hasKey(parsed, 'level1')) {
611
+ const level1 = parsed.level1;
612
+ if (isRecord(level1)) {
613
+ expect(Object.keys(level1)).toStrictEqual(['a', 'z']);
614
+ if (
615
+ hasKey(level1, 'a') &&
616
+ isRecord(level1.a) &&
617
+ hasKey(level1.a, 'nested')
618
+ ) {
619
+ const nested = level1.a.nested;
620
+ if (isRecord(nested)) {
621
+ expect(Object.keys(nested)).toStrictEqual(['x', 'y']);
622
+ }
623
+ }
624
+ }
625
+ }
626
+ }
627
+ });
628
+ });