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.
- package/LICENSE +201 -0
- package/README.md +534 -0
- package/package.json +101 -0
- package/src/array/array-utils-creation.test.mts +443 -0
- package/src/array/array-utils-modification.test.mts +197 -0
- package/src/array/array-utils-overload-type-error.test.mts +149 -0
- package/src/array/array-utils-reducing-value.test.mts +425 -0
- package/src/array/array-utils-search.test.mts +169 -0
- package/src/array/array-utils-set-op.test.mts +335 -0
- package/src/array/array-utils-slice-clamped.test.mts +113 -0
- package/src/array/array-utils-slicing.test.mts +316 -0
- package/src/array/array-utils-transformation.test.mts +790 -0
- package/src/array/array-utils-validation.test.mts +492 -0
- package/src/array/array-utils.mts +4000 -0
- package/src/array/array.test.mts +146 -0
- package/src/array/index.mts +2 -0
- package/src/array/tuple-utils.mts +519 -0
- package/src/array/tuple-utils.test.mts +518 -0
- package/src/collections/imap-mapped.mts +801 -0
- package/src/collections/imap-mapped.test.mts +860 -0
- package/src/collections/imap.mts +651 -0
- package/src/collections/imap.test.mts +932 -0
- package/src/collections/index.mts +6 -0
- package/src/collections/iset-mapped.mts +889 -0
- package/src/collections/iset-mapped.test.mts +1187 -0
- package/src/collections/iset.mts +682 -0
- package/src/collections/iset.test.mts +1084 -0
- package/src/collections/queue.mts +390 -0
- package/src/collections/queue.test.mts +282 -0
- package/src/collections/stack.mts +423 -0
- package/src/collections/stack.test.mts +225 -0
- package/src/expect-type.mts +206 -0
- package/src/functional/index.mts +4 -0
- package/src/functional/match.mts +300 -0
- package/src/functional/match.test.mts +177 -0
- package/src/functional/optional.mts +733 -0
- package/src/functional/optional.test.mts +619 -0
- package/src/functional/pipe.mts +212 -0
- package/src/functional/pipe.test.mts +85 -0
- package/src/functional/result.mts +1134 -0
- package/src/functional/result.test.mts +777 -0
- package/src/globals.d.mts +38 -0
- package/src/guard/has-key.mts +119 -0
- package/src/guard/has-key.test.mts +219 -0
- package/src/guard/index.mts +7 -0
- package/src/guard/is-non-empty-string.mts +108 -0
- package/src/guard/is-non-empty-string.test.mts +91 -0
- package/src/guard/is-non-null-object.mts +106 -0
- package/src/guard/is-non-null-object.test.mts +90 -0
- package/src/guard/is-primitive.mts +165 -0
- package/src/guard/is-primitive.test.mts +102 -0
- package/src/guard/is-record.mts +153 -0
- package/src/guard/is-record.test.mts +112 -0
- package/src/guard/is-type.mts +450 -0
- package/src/guard/is-type.test.mts +496 -0
- package/src/guard/key-is-in.mts +163 -0
- package/src/guard/key-is-in.test.mts +19 -0
- package/src/index.mts +10 -0
- package/src/iterator/index.mts +1 -0
- package/src/iterator/range.mts +120 -0
- package/src/iterator/range.test.mts +33 -0
- package/src/json/index.mts +1 -0
- package/src/json/json.mts +711 -0
- package/src/json/json.test.mts +628 -0
- package/src/number/branded-types/finite-number.mts +354 -0
- package/src/number/branded-types/finite-number.test.mts +135 -0
- package/src/number/branded-types/index.mts +26 -0
- package/src/number/branded-types/int.mts +278 -0
- package/src/number/branded-types/int.test.mts +140 -0
- package/src/number/branded-types/int16.mts +192 -0
- package/src/number/branded-types/int16.test.mts +170 -0
- package/src/number/branded-types/int32.mts +193 -0
- package/src/number/branded-types/int32.test.mts +170 -0
- package/src/number/branded-types/non-negative-finite-number.mts +223 -0
- package/src/number/branded-types/non-negative-finite-number.test.mts +188 -0
- package/src/number/branded-types/non-negative-int16.mts +187 -0
- package/src/number/branded-types/non-negative-int16.test.mts +201 -0
- package/src/number/branded-types/non-negative-int32.mts +187 -0
- package/src/number/branded-types/non-negative-int32.test.mts +204 -0
- package/src/number/branded-types/non-zero-finite-number.mts +229 -0
- package/src/number/branded-types/non-zero-finite-number.test.mts +198 -0
- package/src/number/branded-types/non-zero-int.mts +167 -0
- package/src/number/branded-types/non-zero-int.test.mts +177 -0
- package/src/number/branded-types/non-zero-int16.mts +196 -0
- package/src/number/branded-types/non-zero-int16.test.mts +195 -0
- package/src/number/branded-types/non-zero-int32.mts +196 -0
- package/src/number/branded-types/non-zero-int32.test.mts +197 -0
- package/src/number/branded-types/non-zero-safe-int.mts +196 -0
- package/src/number/branded-types/non-zero-safe-int.test.mts +232 -0
- package/src/number/branded-types/non-zero-uint16.mts +189 -0
- package/src/number/branded-types/non-zero-uint16.test.mts +199 -0
- package/src/number/branded-types/non-zero-uint32.mts +189 -0
- package/src/number/branded-types/non-zero-uint32.test.mts +199 -0
- package/src/number/branded-types/positive-finite-number.mts +241 -0
- package/src/number/branded-types/positive-finite-number.test.mts +204 -0
- package/src/number/branded-types/positive-int.mts +304 -0
- package/src/number/branded-types/positive-int.test.mts +176 -0
- package/src/number/branded-types/positive-int16.mts +188 -0
- package/src/number/branded-types/positive-int16.test.mts +197 -0
- package/src/number/branded-types/positive-int32.mts +188 -0
- package/src/number/branded-types/positive-int32.test.mts +197 -0
- package/src/number/branded-types/positive-safe-int.mts +187 -0
- package/src/number/branded-types/positive-safe-int.test.mts +210 -0
- package/src/number/branded-types/positive-uint16.mts +188 -0
- package/src/number/branded-types/positive-uint16.test.mts +203 -0
- package/src/number/branded-types/positive-uint32.mts +188 -0
- package/src/number/branded-types/positive-uint32.test.mts +203 -0
- package/src/number/branded-types/safe-int.mts +291 -0
- package/src/number/branded-types/safe-int.test.mts +170 -0
- package/src/number/branded-types/safe-uint.mts +187 -0
- package/src/number/branded-types/safe-uint.test.mts +176 -0
- package/src/number/branded-types/uint.mts +179 -0
- package/src/number/branded-types/uint.test.mts +158 -0
- package/src/number/branded-types/uint16.mts +186 -0
- package/src/number/branded-types/uint16.test.mts +170 -0
- package/src/number/branded-types/uint32.mts +218 -0
- package/src/number/branded-types/uint32.test.mts +170 -0
- package/src/number/enum/index.mts +2 -0
- package/src/number/enum/int8.mts +344 -0
- package/src/number/enum/int8.test.mts +180 -0
- package/src/number/enum/uint8.mts +293 -0
- package/src/number/enum/uint8.test.mts +164 -0
- package/src/number/index.mts +4 -0
- package/src/number/num.mts +604 -0
- package/src/number/num.test.mts +242 -0
- package/src/number/refined-number-utils.mts +566 -0
- package/src/object/index.mts +1 -0
- package/src/object/object.mts +447 -0
- package/src/object/object.test.mts +124 -0
- package/src/others/cast-mutable.mts +113 -0
- package/src/others/cast-readonly.mts +192 -0
- package/src/others/cast-readonly.test.mts +89 -0
- package/src/others/if-then.mts +98 -0
- package/src/others/if-then.test.mts +75 -0
- package/src/others/index.mts +7 -0
- package/src/others/map-nullable.mts +172 -0
- package/src/others/map-nullable.test.mts +297 -0
- package/src/others/memoize-function.mts +196 -0
- package/src/others/memoize-function.test.mts +168 -0
- package/src/others/tuple.mts +160 -0
- package/src/others/tuple.test.mts +11 -0
- package/src/others/unknown-to-string.mts +215 -0
- 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
|
+
});
|