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,711 @@
1
+ import { Arr } from '../array/index.mjs';
2
+ import { pipe, Result } from '../functional/index.mjs';
3
+ import { isRecord } from '../guard/index.mjs';
4
+ import { castMutable } from '../others/index.mjs';
5
+ import { unknownToString } from '../others/unknown-to-string.mjs';
6
+
7
+ /**
8
+ * A collection of type-safe JSON utility functions that provide safe parsing,
9
+ * stringification, and manipulation of JSON data. All functions return `Result`
10
+ * types to handle errors without throwing exceptions.
11
+ *
12
+ * @example Basic usage
13
+ * ```typescript
14
+ * import { Json, Result } from 'ts-data-forge';
15
+ *
16
+ * // Parse JSON safely
17
+ * const parseResult = Json.parse('{"name": "Alice", "age": 30}');
18
+ * if (Result.isOk(parseResult)) {
19
+ * console.log(parseResult.value); // { name: 'Alice', age: 30 }
20
+ * }
21
+ *
22
+ * // Stringify with error handling
23
+ * const stringifyResult = Json.stringify({ name: 'Bob', age: 25 });
24
+ * if (Result.isOk(stringifyResult)) {
25
+ * console.log(stringifyResult.value); // '{"name":"Bob","age":25}'
26
+ * }
27
+ * ```
28
+ */
29
+ export namespace Json {
30
+ /**
31
+ * Safely converts a JSON string into a JavaScript value without throwing exceptions.
32
+ *
33
+ * This function provides type-safe JSON parsing by wrapping the native `JSON.parse`
34
+ * in a `Result` type, allowing you to handle parsing errors gracefully without
35
+ * try-catch blocks.
36
+ *
37
+ * @param text - A valid JSON string to parse. Can contain any valid JSON data type:
38
+ * primitives (string, number, boolean, null), arrays, or objects.
39
+ * @param reviver - Optional function that transforms parsed values. Called for each
40
+ * key-value pair in the JSON. The function receives the key name and parsed value,
41
+ * and should return the transformed value. For nested objects, inner objects are
42
+ * processed before outer objects.
43
+ * @returns A `Result<JsonValue, string>` containing:
44
+ * - On success: `Result.ok(parsedValue)` where `parsedValue` is the parsed JSON
45
+ * - On failure: `Result.err(errorMessage)` where `errorMessage` describes the parsing error
46
+ *
47
+ * @example Basic parsing
48
+ * ```typescript
49
+ * // Parse simple values
50
+ * const str = Json.parse('"hello"');
51
+ * // Result.ok('hello')
52
+ *
53
+ * const num = Json.parse('42');
54
+ * // Result.ok(42)
55
+ *
56
+ * const bool = Json.parse('true');
57
+ * // Result.ok(true)
58
+ *
59
+ * const nul = Json.parse('null');
60
+ * // Result.ok(null)
61
+ * ```
62
+ *
63
+ * @example Parsing objects and arrays
64
+ * ```typescript
65
+ * const obj = Json.parse('{"name": "John", "age": 30, "active": true}');
66
+ * if (Result.isOk(obj)) {
67
+ * console.log(obj.value.name); // 'John'
68
+ * console.log(obj.value.age); // 30
69
+ * }
70
+ *
71
+ * const arr = Json.parse('[1, "two", true, null]');
72
+ * if (Result.isOk(arr)) {
73
+ * console.log(arr.value); // [1, 'two', true, null]
74
+ * }
75
+ * ```
76
+ *
77
+ * @example Error handling
78
+ * ```typescript
79
+ * const invalid = Json.parse('invalid json');
80
+ * if (Result.isErr(invalid)) {
81
+ * console.log('Parse failed:', invalid.value);
82
+ * // Parse failed: Unexpected token i in JSON at position 0
83
+ * }
84
+ *
85
+ * const malformed = Json.parse('{"missing": quote}');
86
+ * if (Result.isErr(malformed)) {
87
+ * console.log('Parse failed:', malformed.value);
88
+ * // Parse failed: Unexpected token q in JSON at position 12
89
+ * }
90
+ * ```
91
+ *
92
+ * @example Using reviver for data transformation
93
+ * ```typescript
94
+ * // Convert ISO date strings to Date objects
95
+ * const dateReviver = (key: string, value: unknown): unknown => {
96
+ * if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
97
+ * return new Date(value);
98
+ * }
99
+ * return value;
100
+ * };
101
+ *
102
+ * const result = Json.parse(
103
+ * '{"event": "Meeting", "date": "2023-12-25T10:00:00Z"}',
104
+ * dateReviver
105
+ * );
106
+ * if (Result.isOk(result)) {
107
+ * console.log(result.value.date instanceof Date); // true
108
+ * }
109
+ * ```
110
+ *
111
+ * @example API response validation
112
+ * ```typescript
113
+ * import { isRecord, hasKey } from 'ts-data-forge';
114
+ *
115
+ * const validateApiResponse = (jsonString: string) => {
116
+ * const parseResult = Json.parse(jsonString);
117
+ *
118
+ * if (Result.isErr(parseResult)) {
119
+ * return Result.err(`Invalid JSON: ${parseResult.value}`);
120
+ * }
121
+ *
122
+ * const data = parseResult.value;
123
+ * if (!isRecord(data) || !hasKey(data, 'status')) {
124
+ * return Result.err('Missing required "status" field');
125
+ * }
126
+ *
127
+ * return Result.ok(data);
128
+ * };
129
+ * ```
130
+ */
131
+ export const parse = (
132
+ text: string,
133
+ reviver?: (this: unknown, key: string, value: JsonValue) => unknown,
134
+ ): Result<JsonValue, string> => {
135
+ try {
136
+ return Result.ok(
137
+ JSON.parse(
138
+ text,
139
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
140
+ reviver as (this: unknown, key: string, value: unknown) => unknown,
141
+ ),
142
+ );
143
+ } catch (error: unknown) {
144
+ const errStr = unknownToString(error);
145
+ return Result.err(
146
+ Result.isOk(errStr) ? errStr.value : 'Failed to parse JSON',
147
+ );
148
+ }
149
+ };
150
+
151
+ /**
152
+ * Safely converts a JavaScript value to a JSON string without throwing exceptions.
153
+ *
154
+ * This function provides type-safe JSON stringification by wrapping the native
155
+ * `JSON.stringify` in a `Result` type, allowing you to handle serialization errors
156
+ * gracefully (such as circular references or BigInt values).
157
+ *
158
+ * @param value - The JavaScript value to serialize. Can be any value that JSON.stringify
159
+ * accepts: primitives, objects, arrays. Non-serializable values (functions, undefined,
160
+ * symbols) will be omitted or converted to null according to JSON.stringify behavior.
161
+ * @param replacer - Optional function that transforms values during serialization.
162
+ * Called for each key-value pair. Should return the value to be serialized, or
163
+ * undefined to omit the property from the result.
164
+ * @param space - Optional parameter for formatting the output JSON:
165
+ * - Number (1-10): Number of spaces to indent each level
166
+ * - String: String to use for indentation (first 10 characters)
167
+ * - undefined/null: No formatting (compact output)
168
+ * @returns A `Result<string, string>` containing:
169
+ * - On success: `Result.ok(jsonString)` where `jsonString` is the serialized JSON
170
+ * - On failure: `Result.err(errorMessage)` where `errorMessage` describes the error
171
+ *
172
+ * @example Basic stringification
173
+ * ```typescript
174
+ * // Primitives
175
+ * const str = Json.stringify('hello');
176
+ * // Result.ok('"hello"')
177
+ *
178
+ * const num = Json.stringify(42);
179
+ * // Result.ok('42')
180
+ *
181
+ * const bool = Json.stringify(true);
182
+ * // Result.ok('true')
183
+ *
184
+ * const nul = Json.stringify(null);
185
+ * // Result.ok('null')
186
+ * ```
187
+ *
188
+ * @example Objects and arrays
189
+ * ```typescript
190
+ * const obj = { name: 'John', age: 30, active: true };
191
+ * const result = Json.stringify(obj);
192
+ * if (Result.isOk(result)) {
193
+ * console.log(result.value); // '{"name":"John","age":30,"active":true}'
194
+ * }
195
+ *
196
+ * const arr = [1, 'two', true, null];
197
+ * const arrResult = Json.stringify(arr);
198
+ * if (Result.isOk(arrResult)) {
199
+ * console.log(arrResult.value); // '[1,"two",true,null]'
200
+ * }
201
+ * ```
202
+ *
203
+ * @example Formatted output
204
+ * ```typescript
205
+ * const data = { users: [{ name: 'Alice', id: 1 }, { name: 'Bob', id: 2 }] };
206
+ *
207
+ * // Pretty-print with 2 spaces
208
+ * const formatted = Json.stringify(data, undefined, 2);
209
+ * if (Result.isOk(formatted)) {
210
+ * console.log(formatted.value);
211
+ * // {
212
+ * // "users": [
213
+ * // {
214
+ * // "name": "Alice",
215
+ * // "id": 1
216
+ * // },
217
+ * // {
218
+ * // "name": "Bob",
219
+ * // "id": 2
220
+ * // }
221
+ * // ]
222
+ * // }
223
+ * }
224
+ *
225
+ * // Custom indentation string
226
+ * const tabbed = Json.stringify(data, undefined, '\t');
227
+ * ```
228
+ *
229
+ * @example Error handling for problematic values
230
+ * ```typescript
231
+ * // Circular reference error
232
+ * const circular: any = { name: 'test' };
233
+ * circular.self = circular;
234
+ * const error = Json.stringify(circular);
235
+ * if (Result.isErr(error)) {
236
+ * console.log('Stringify failed:', error.value);
237
+ * // Stringify failed: Converting circular structure to JSON
238
+ * }
239
+ *
240
+ * // BigInt error
241
+ * const bigIntError = Json.stringify({ big: BigInt(123) });
242
+ * if (Result.isErr(bigIntError)) {
243
+ * console.log('Stringify failed:', bigIntError.value);
244
+ * // Stringify failed: Do not know how to serialize a BigInt
245
+ * }
246
+ * ```
247
+ *
248
+ * @example Using replacer for value transformation
249
+ * ```typescript
250
+ * // Filter out sensitive data
251
+ * const sensitiveData = { name: 'John', password: 'secret123', email: 'john@example.com' };
252
+ *
253
+ * const secureReplacer = (key: string, value: unknown) => {
254
+ * if (key === 'password') return '[REDACTED]';
255
+ * return value;
256
+ * };
257
+ *
258
+ * const safe = Json.stringify(sensitiveData, secureReplacer);
259
+ * if (Result.isOk(safe)) {
260
+ * console.log(safe.value);
261
+ * // '{"name":"John","password":"[REDACTED]","email":"john@example.com"}'
262
+ * }
263
+ *
264
+ * // Convert Dates to custom format
265
+ * const data = { event: 'Meeting', date: new Date('2023-12-25T10:00:00Z') };
266
+ * const dateReplacer = (key: string, value: unknown) => {
267
+ * if (value instanceof Date) return value.toISOString().split('T')[0]; // YYYY-MM-DD
268
+ * return value;
269
+ * };
270
+ *
271
+ * const result = Json.stringify(data, dateReplacer);
272
+ * if (Result.isOk(result)) {
273
+ * console.log(result.value); // '{"event":"Meeting","date":"2023-12-25"}'
274
+ * }
275
+ * ```
276
+ *
277
+ * @example Special value handling
278
+ * ```typescript
279
+ * // Functions, undefined, and symbols are omitted in objects
280
+ * const obj = {
281
+ * name: 'test',
282
+ * fn: () => {}, // omitted
283
+ * undef: undefined, // omitted
284
+ * sym: Symbol('test'), // omitted
285
+ * num: 42
286
+ * };
287
+ * const result = Json.stringify(obj);
288
+ * // Result.ok('{"name":"test","num":42}')
289
+ *
290
+ * // In arrays, they become null
291
+ * const arr = ['string', () => {}, undefined, Symbol('test'), 42];
292
+ * const arrResult = Json.stringify(arr);
293
+ * // Result.ok('["string",null,null,null,42]')
294
+ * ```
295
+ */
296
+ export const stringify = (
297
+ value: unknown,
298
+ replacer?: (this: unknown, key: string, val: unknown) => unknown,
299
+ space?: UintRangeInclusive<1, 10> | string,
300
+ ): Result<string, string> => {
301
+ try {
302
+ return Result.ok(JSON.stringify(value, replacer, space));
303
+ } catch (error) {
304
+ const errStr = unknownToString(error);
305
+ return Result.err(
306
+ Result.isOk(errStr) ? errStr.value : 'Failed to stringify JSON',
307
+ );
308
+ }
309
+ };
310
+
311
+ /**
312
+ * Safely converts a JavaScript value to a JSON string, including only the specified properties.
313
+ *
314
+ * This function provides selective serialization by allowing you to specify exactly which
315
+ * object properties should be included in the resulting JSON. It's useful for creating
316
+ * filtered or minimal representations of objects, such as for API responses or logging.
317
+ *
318
+ * @param value - The JavaScript value to serialize. While any value is accepted,
319
+ * the property filtering only applies to objects and nested objects.
320
+ * @param propertiesToBeSelected - Optional array of property names (strings) and array
321
+ * indices (numbers) to include in the serialization. If provided, only these properties
322
+ * will appear in the output JSON. If undefined, all properties are included.
323
+ * @param space - Optional formatting parameter:
324
+ * - Number (1-10): Number of spaces to indent each level
325
+ * - String: String to use for indentation (first 10 characters)
326
+ * - undefined/null: No formatting (compact output)
327
+ * @returns A `Result<string, string>` containing:
328
+ * - On success: `Result.ok(jsonString)` with only selected properties
329
+ * - On failure: `Result.err(errorMessage)` describing the serialization error
330
+ *
331
+ * @example Basic property selection
332
+ * ```typescript
333
+ * const user = {
334
+ * id: 1,
335
+ * name: 'Alice',
336
+ * email: 'alice@example.com',
337
+ * password: 'secret123',
338
+ * lastLogin: '2023-12-01'
339
+ * };
340
+ *
341
+ * // Include only public fields
342
+ * const publicFields = Json.stringifySelected(user, ['id', 'name', 'email']);
343
+ * if (Result.isOk(publicFields)) {
344
+ * console.log(publicFields.value);
345
+ * // '{"id":1,"name":"Alice","email":"alice@example.com"}'
346
+ * }
347
+ * ```
348
+ *
349
+ * @example Array property selection
350
+ * ```typescript
351
+ * const data = {
352
+ * users: [
353
+ * { id: 1, name: 'Alice', secret: 'hidden1' },
354
+ * { id: 2, name: 'Bob', secret: 'hidden2' }
355
+ * ],
356
+ * metadata: { total: 2, page: 1 }
357
+ * };
358
+ *
359
+ * // Select specific properties across nested structures
360
+ * const selected = Json.stringifySelected(data, ['users', 'id', 'name', 'total']);
361
+ * if (Result.isOk(selected)) {
362
+ * console.log(selected.value);
363
+ * // '{"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}],"metadata":{"total":2}}'
364
+ * }
365
+ * ```
366
+ *
367
+ * @example Formatted output with selection
368
+ * ```typescript
369
+ * const config = {
370
+ * database: { host: 'localhost', password: 'secret' },
371
+ * api: { endpoint: '/api/v1', key: 'secret-key' },
372
+ * features: { debug: true, logging: true }
373
+ * };
374
+ *
375
+ * // Create a safe config for logging (exclude sensitive fields)
376
+ * const safeConfig = Json.stringifySelected(
377
+ * config,
378
+ * ['database', 'host', 'api', 'endpoint', 'features'],
379
+ * 2
380
+ * );
381
+ * if (Result.isOk(safeConfig)) {
382
+ * console.log(safeConfig.value);
383
+ * // {
384
+ * // "database": {
385
+ * // "host": "localhost"
386
+ * // },
387
+ * // "api": {
388
+ * // "endpoint": "/api/v1"
389
+ * // },
390
+ * // "features": {
391
+ * // "debug": true,
392
+ * // "logging": true
393
+ * // }
394
+ * // }
395
+ * }
396
+ * ```
397
+ *
398
+ * @example Working with arrays and indices
399
+ * ```typescript
400
+ * const matrix = [
401
+ * [1, 2, 3, 4],
402
+ * [5, 6, 7, 8],
403
+ * [9, 10, 11, 12]
404
+ * ];
405
+ *
406
+ * // Select only first two columns (indices 0 and 1)
407
+ * const columns = Json.stringifySelected(matrix, [0, 1]);
408
+ * if (Result.isOk(columns)) {
409
+ * console.log(columns.value);
410
+ * // '[[1,2],[5,6],[9,10]]'
411
+ * }
412
+ * ```
413
+ *
414
+ * @example API response filtering
415
+ * ```typescript
416
+ * // Simulate filtering an API response
417
+ * const fullResponse = {
418
+ * data: {
419
+ * users: [
420
+ * { id: 1, name: 'Alice', email: 'alice@example.com', internalId: 'usr_123' },
421
+ * { id: 2, name: 'Bob', email: 'bob@example.com', internalId: 'usr_456' }
422
+ * ]
423
+ * },
424
+ * metadata: {
425
+ * total: 2,
426
+ * internalVersion: '1.2.3',
427
+ * serverTime: Date.now()
428
+ * }
429
+ * };
430
+ *
431
+ * // Create public API response
432
+ * const publicResponse = Json.stringifySelected(
433
+ * fullResponse,
434
+ * ['data', 'users', 'id', 'name', 'email', 'metadata', 'total']
435
+ * );
436
+ *
437
+ * if (Result.isOk(publicResponse)) {
438
+ * console.log('Public API response:', publicResponse.value);
439
+ * // Only includes id, name, email for users and total in metadata
440
+ * }
441
+ * ```
442
+ *
443
+ * @example Error handling
444
+ * ```typescript
445
+ * const circular: any = { name: 'test' };
446
+ * circular.self = circular;
447
+ *
448
+ * const result = Json.stringifySelected(circular, ['name', 'self']);
449
+ * if (Result.isErr(result)) {
450
+ * console.log('Serialization failed:', result.value);
451
+ * // Even with property selection, circular references still cause errors
452
+ * }
453
+ * ```
454
+ */
455
+ export const stringifySelected = (
456
+ value: unknown,
457
+ propertiesToBeSelected?: readonly (number | string)[],
458
+ space?: UintRangeInclusive<1, 10> | string,
459
+ ): Result<string, string> => {
460
+ try {
461
+ return Result.ok(
462
+ JSON.stringify(value, castMutable(propertiesToBeSelected), space),
463
+ );
464
+ } catch (error) {
465
+ const errStr = unknownToString(error);
466
+ return Result.err(
467
+ Result.isOk(errStr) ? errStr.value : 'Failed to stringify JSON',
468
+ );
469
+ }
470
+ };
471
+
472
+ /**
473
+ * Safely converts a JavaScript record to a JSON string with keys sorted alphabetically at all levels.
474
+ *
475
+ * This function creates deterministic JSON output by ensuring that object keys appear in
476
+ * alphabetical order at every level of nesting. This is particularly useful for creating
477
+ * consistent output for comparison, hashing, caching, or when you need reproducible JSON
478
+ * representations across different JavaScript engines or runs.
479
+ *
480
+ * @param value - An object (`UnknownRecord`) to serialize. Must be a plain object
481
+ * (not an array, primitive, or null). Nested objects and arrays within the object
482
+ * will also have their keys sorted alphabetically.
483
+ * @param space - Optional formatting parameter:
484
+ * - Number (1-10): Number of spaces to indent each level
485
+ * - String: String to use for indentation (first 10 characters)
486
+ * - undefined/null: No formatting (compact output)
487
+ * @returns A `Result<string, string>` containing:
488
+ * - On success: `Result.ok(jsonString)` with all object keys sorted alphabetically
489
+ * - On failure: `Result.err(errorMessage)` describing the serialization error
490
+ *
491
+ * @example Basic key sorting
492
+ * ```typescript
493
+ * const unsortedObj = {
494
+ * zebra: 'animal',
495
+ * apple: 'fruit',
496
+ * banana: 'fruit',
497
+ * aardvark: 'animal'
498
+ * };
499
+ *
500
+ * const sorted = Json.stringifySortedKey(unsortedObj);
501
+ * if (Result.isOk(sorted)) {
502
+ * console.log(sorted.value);
503
+ * // '{"aardvark":"animal","apple":"fruit","banana":"fruit","zebra":"animal"}'
504
+ * }
505
+ *
506
+ * // Compare with regular stringify (order not guaranteed)
507
+ * const regular = Json.stringify(unsortedObj);
508
+ * // Keys might appear in insertion order or engine-dependent order
509
+ * ```
510
+ *
511
+ * @example Nested object key sorting
512
+ * ```typescript
513
+ * const nestedObj = {
514
+ * user: {
515
+ * name: 'Alice',
516
+ * age: 30,
517
+ * address: {
518
+ * zip: '12345',
519
+ * city: 'New York',
520
+ * country: 'USA'
521
+ * }
522
+ * },
523
+ * settings: {
524
+ * theme: 'dark',
525
+ * language: 'en',
526
+ * notifications: {
527
+ * email: true,
528
+ * sms: false,
529
+ * push: true
530
+ * }
531
+ * }
532
+ * };
533
+ *
534
+ * const sorted = Json.stringifySortedKey(nestedObj, 2);
535
+ * if (Result.isOk(sorted)) {
536
+ * console.log(sorted.value);
537
+ * // {
538
+ * // "settings": {
539
+ * // "language": "en",
540
+ * // "notifications": {
541
+ * // "email": true,
542
+ * // "push": true,
543
+ * // "sms": false
544
+ * // },
545
+ * // "theme": "dark"
546
+ * // },
547
+ * // "user": {
548
+ * // "address": {
549
+ * // "city": "New York",
550
+ * // "country": "USA",
551
+ * // "zip": "12345"
552
+ * // },
553
+ * // "age": 30,
554
+ * // "name": "Alice"
555
+ * // }
556
+ * // }
557
+ * }
558
+ * ```
559
+ *
560
+ * @example Arrays with nested objects
561
+ * ```typescript
562
+ * const dataWithArrays = {
563
+ * users: [
564
+ * { name: 'Bob', id: 2, active: true },
565
+ * { name: 'Alice', id: 1, active: false }
566
+ * ],
567
+ * metadata: {
568
+ * version: '1.0',
569
+ * created: '2023-12-01',
570
+ * author: 'system'
571
+ * }
572
+ * };
573
+ *
574
+ * const sorted = Json.stringifySortedKey(dataWithArrays);
575
+ * if (Result.isOk(sorted)) {
576
+ * console.log(sorted.value);
577
+ * // Keys in objects within arrays are also sorted:
578
+ * // '{"metadata":{"author":"system","created":"2023-12-01","version":"1.0"},"users":[{"active":true,"id":2,"name":"Bob"},{"active":false,"id":1,"name":"Alice"}]}'
579
+ * }
580
+ * ```
581
+ *
582
+ * @example Configuration files and consistent output
583
+ * ```typescript
584
+ * // Useful for configuration files that need consistent ordering
585
+ * const config = {
586
+ * database: {
587
+ * port: 5432,
588
+ * host: 'localhost',
589
+ * name: 'myapp',
590
+ * username: 'admin'
591
+ * },
592
+ * server: {
593
+ * port: 3000,
594
+ * middleware: ['cors', 'helmet', 'compression'],
595
+ * routes: {
596
+ * api: '/api/v1',
597
+ * health: '/health',
598
+ * docs: '/docs'
599
+ * }
600
+ * }
601
+ * };
602
+ *
603
+ * const consistentConfig = Json.stringifySortedKey(config, 2);
604
+ * if (Result.isOk(consistentConfig)) {
605
+ * // Always produces the same key order regardless of object creation order
606
+ * console.log('Consistent config:', consistentConfig.value);
607
+ * }
608
+ * ```
609
+ *
610
+ * @example Deterministic hashing and comparison
611
+ * ```typescript
612
+ * // Useful when you need consistent JSON for hashing or comparison
613
+ * const createHash = async (obj: Record<string, unknown>) => {
614
+ * const sortedJson = Json.stringifySortedKey(obj);
615
+ * if (Result.isErr(sortedJson)) {
616
+ * throw new Error(`Failed to serialize: ${sortedJson.value}`);
617
+ * }
618
+ *
619
+ * // Now you can safely hash the JSON string
620
+ * const encoder = new TextEncoder();
621
+ * const data = encoder.encode(sortedJson.value);
622
+ * const hashBuffer = await crypto.subtle.digest('SHA-256', data);
623
+ * return Array.from(new Uint8Array(hashBuffer))
624
+ * .map(b => b.toString(16).padStart(2, '0'))
625
+ * .join('');
626
+ * };
627
+ *
628
+ * // These will produce the same hash regardless of property order:
629
+ * const obj1 = { b: 2, a: 1, c: 3 };
630
+ * const obj2 = { a: 1, c: 3, b: 2 };
631
+ * // createHash(obj1) === createHash(obj2) // true
632
+ * ```
633
+ *
634
+ * @example Error handling
635
+ * ```typescript
636
+ * const problematicObj = {
637
+ * normal: 'value',
638
+ * circular: {} as any
639
+ * };
640
+ * problematicObj.circular.self = problematicObj;
641
+ *
642
+ * const result = Json.stringifySortedKey(problematicObj);
643
+ * if (Result.isErr(result)) {
644
+ * console.log('Serialization failed:', result.value);
645
+ * // Even with key sorting, circular references still cause errors
646
+ * }
647
+ * ```
648
+ */
649
+ export const stringifySortedKey = (
650
+ value: UnknownRecord,
651
+ space?: UintRangeInclusive<1, 10> | string,
652
+ ): Result<string, string> => {
653
+ const allKeys = pipe(keysDeep(value))
654
+ .map((keys) => Arr.uniq(keys))
655
+ .map((ks) => ks.toSorted()).value;
656
+
657
+ return stringifySelected(value, allKeys, space);
658
+ };
659
+ }
660
+
661
+ /**
662
+ * @internal
663
+ * Recursively collects all property keys from a nested object structure.
664
+ *
665
+ * This helper function traverses an object and its nested objects and arrays,
666
+ * collecting all string keys found at any level of nesting. The function mutates
667
+ * the provided keys array for performance reasons.
668
+ *
669
+ * @param obj - The record to extract keys from. Must be a plain object.
670
+ * @param mut_keys - A mutable array to accumulate the collected keys. This array
671
+ * will be modified in-place by the function for performance reasons.
672
+ */
673
+ const keysDeepImpl = (
674
+ obj: UnknownRecord,
675
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
676
+ mut_keys: string[],
677
+ ): void => {
678
+ for (const k of Object.keys(obj)) {
679
+ mut_keys.push(k);
680
+ const o = obj[k];
681
+ if (isRecord(o)) {
682
+ keysDeepImpl(o, mut_keys);
683
+ }
684
+ if (Array.isArray(o)) {
685
+ for (const li of o) {
686
+ if (isRecord(li)) {
687
+ keysDeepImpl(li, mut_keys);
688
+ }
689
+ }
690
+ }
691
+ }
692
+ };
693
+
694
+ /**
695
+ * @internal
696
+ * Extracts all property keys from a nested object structure into a flat array.
697
+ *
698
+ * This function serves as a safe wrapper around `keysDeepImpl`, creating a new
699
+ * mutable array and passing it to the recursive implementation. The result
700
+ * contains all keys found at any level of nesting within the input object.
701
+ *
702
+ * @param obj - The record to extract keys from. Must be a plain object.
703
+ * @returns A readonly array of all string keys found in the object and its
704
+ * nested objects/arrays. May contain duplicates if the same key appears
705
+ * at multiple levels.
706
+ */
707
+ const keysDeep = (obj: UnknownRecord): readonly string[] => {
708
+ const mut_keys: string[] = [];
709
+ keysDeepImpl(obj, mut_keys);
710
+ return mut_keys;
711
+ };