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,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
|
+
};
|