@macroforge/mcp-server 0.1.37 → 0.1.39
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/docs/api/api-overview.md +13 -13
- package/docs/api/expand-sync.md +8 -8
- package/docs/api/native-plugin.md +15 -15
- package/docs/api/position-mapper.md +6 -6
- package/docs/api/transform-sync.md +11 -11
- package/docs/builtin-macros/clone.md +43 -23
- package/docs/builtin-macros/debug.md +50 -18
- package/docs/builtin-macros/default.md +79 -28
- package/docs/builtin-macros/deserialize/cycleforward-reference-support.md +11 -0
- package/docs/builtin-macros/deserialize/example.md +1625 -0
- package/docs/builtin-macros/deserialize/overview.md +15 -10
- package/docs/builtin-macros/deserialize/union-type-deserialization.md +27 -0
- package/docs/builtin-macros/deserialize/validation.md +34 -0
- package/docs/builtin-macros/deserialize.md +1608 -23
- package/docs/builtin-macros/hash.md +87 -20
- package/docs/builtin-macros/macros-overview.md +40 -40
- package/docs/builtin-macros/ord.md +56 -31
- package/docs/builtin-macros/partial-eq/example.md +526 -0
- package/docs/builtin-macros/partial-eq/overview.md +39 -0
- package/docs/builtin-macros/partial-eq.md +184 -26
- package/docs/builtin-macros/partial-ord.md +68 -30
- package/docs/builtin-macros/serialize/example.md +139 -0
- package/docs/builtin-macros/serialize/overview.md +32 -0
- package/docs/builtin-macros/serialize/type-specific-serialization.md +22 -0
- package/docs/builtin-macros/serialize.md +130 -28
- package/docs/concepts/architecture.md +2 -2
- package/docs/concepts/derive-system.md +25 -39
- package/docs/concepts/how-macros-work.md +8 -4
- package/docs/custom-macros/custom-overview.md +23 -23
- package/docs/custom-macros/rust-setup.md +31 -31
- package/docs/custom-macros/ts-macro-derive.md +107 -107
- package/docs/custom-macros/ts-quote.md +226 -226
- package/docs/getting-started/first-macro.md +38 -28
- package/docs/getting-started/installation.md +15 -15
- package/docs/integration/cli.md +9 -9
- package/docs/integration/configuration.md +16 -16
- package/docs/integration/mcp-server.md +6 -6
- package/docs/integration/svelte-preprocessor.md +40 -41
- package/docs/integration/typescript-plugin.md +13 -12
- package/docs/integration/vite-plugin.md +12 -12
- package/docs/language-servers/zed.md +1 -1
- package/docs/sections.json +88 -2
- package/package.json +2 -2
|
@@ -8,18 +8,10 @@ safe parsing of complex JSON structures including circular references.
|
|
|
8
8
|
|
|
9
9
|
| Type | Generated Code | Description |
|
|
10
10
|
|------|----------------|-------------|
|
|
11
|
-
| Class | `
|
|
12
|
-
| Enum | `
|
|
13
|
-
| Interface | `
|
|
14
|
-
| Type Alias | `
|
|
15
|
-
|
|
16
|
-
## Configuration
|
|
17
|
-
|
|
18
|
-
The `functionNamingStyle` option in `macroforge.json` controls naming:
|
|
19
|
-
- `"prefix"` (default): Prefixes with type name (e.g., `myTypeFromStringifiedJSON`)
|
|
20
|
-
- `"suffix"`: Suffixes with type name (e.g., `fromStringifiedJSONMyType`)
|
|
21
|
-
- `"generic"`: Uses TypeScript generics (e.g., `fromStringifiedJSON<T extends MyType>`)
|
|
22
|
-
- `"namespace"`: Legacy namespace wrapping
|
|
11
|
+
| Class | `classNameDeserialize(input)` + `static deserialize(input)` | Standalone function + static factory method |
|
|
12
|
+
| Enum | `enumNameDeserialize(input)`, `enumNameDeserializeWithContext(data)`, `enumNameIs(value)` | Standalone functions |
|
|
13
|
+
| Interface | `interfaceNameDeserialize(input)`, etc. | Standalone functions |
|
|
14
|
+
| Type Alias | `typeNameDeserialize(input)`, etc. | Standalone functions |
|
|
23
15
|
|
|
24
16
|
## Return Type
|
|
25
17
|
|
|
@@ -64,7 +56,7 @@ The macro supports 30+ validators via `@serde(validate(...))`:
|
|
|
64
56
|
|
|
65
57
|
The `@serde` decorator supports:
|
|
66
58
|
|
|
67
|
-
- `skip` / `
|
|
59
|
+
- `skip` / `skipDeserializing` - Exclude field from deserialization
|
|
68
60
|
- `rename = "jsonKey"` - Read from different JSON property
|
|
69
61
|
- `default` / `default = expr` - Use default value if missing
|
|
70
62
|
- `flatten` - Read fields from parent object level
|
|
@@ -72,8 +64,8 @@ The `@serde` decorator supports:
|
|
|
72
64
|
|
|
73
65
|
## Container-Level Options
|
|
74
66
|
|
|
75
|
-
- `
|
|
76
|
-
- `
|
|
67
|
+
- `denyUnknownFields` - Error on unrecognized JSON properties
|
|
68
|
+
- `renameAll = "camelCase"` - Apply naming convention to all fields
|
|
77
69
|
|
|
78
70
|
## Union Type Deserialization
|
|
79
71
|
|
|
@@ -89,7 +81,7 @@ For unions containing primitive types (`string | number`), the deserializer uses
|
|
|
89
81
|
|
|
90
82
|
### Class/Interface Unions
|
|
91
83
|
For unions of serializable types (`User | Admin`), the deserializer requires a
|
|
92
|
-
`__type` field in the JSON to dispatch to the correct type's `
|
|
84
|
+
`__type` field in the JSON to dispatch to the correct type's `deserializeWithContext` method.
|
|
93
85
|
|
|
94
86
|
### Generic Type Parameters
|
|
95
87
|
For generic unions like `type Result<T> = T | Error`, the generic type parameter `T`
|
|
@@ -105,28 +97,1621 @@ Mixed unions (e.g., `string | Date | User`) check in order:
|
|
|
105
97
|
|
|
106
98
|
## Example
|
|
107
99
|
|
|
100
|
+
```typescript before
|
|
101
|
+
/** @derive(Deserialize) @serde({ denyUnknownFields: true }) */
|
|
102
|
+
class User {
|
|
103
|
+
id: number;
|
|
104
|
+
|
|
105
|
+
/** @serde({ validate: { email: true, maxLength: 255 } }) */
|
|
106
|
+
email: string;
|
|
107
|
+
|
|
108
|
+
/** @serde({ default: "guest" }) */
|
|
109
|
+
name: string;
|
|
110
|
+
|
|
111
|
+
/** @serde({ validate: { positive: true } }) */
|
|
112
|
+
age?: number;
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```typescript after
|
|
117
|
+
class User {
|
|
118
|
+
id: number;
|
|
119
|
+
|
|
120
|
+
email: string;
|
|
121
|
+
|
|
122
|
+
name: string;
|
|
123
|
+
|
|
124
|
+
age?: number;
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Generated output:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
class User {
|
|
132
|
+
id: number;
|
|
133
|
+
|
|
134
|
+
email: string;
|
|
135
|
+
|
|
136
|
+
name: string;
|
|
137
|
+
|
|
138
|
+
age?: number;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Generated output:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { DeserializeContext } from 'macroforge/serde';
|
|
146
|
+
import { DeserializeError } from 'macroforge/serde';
|
|
147
|
+
import type { DeserializeOptions } from 'macroforge/serde';
|
|
148
|
+
import { PendingRef } from 'macroforge/serde';
|
|
149
|
+
|
|
150
|
+
/** @serde({ denyUnknownFields: true }) */
|
|
151
|
+
class User {
|
|
152
|
+
id: number;
|
|
153
|
+
|
|
154
|
+
email: string;
|
|
155
|
+
|
|
156
|
+
name: string;
|
|
157
|
+
|
|
158
|
+
age?: number;
|
|
159
|
+
|
|
160
|
+
constructor(props: {
|
|
161
|
+
id: number;
|
|
162
|
+
email: string;
|
|
163
|
+
name?: string;
|
|
164
|
+
age?: number;
|
|
165
|
+
}) {
|
|
166
|
+
this.id = props.id;
|
|
167
|
+
this.email = props.email;
|
|
168
|
+
this.name = props.name as string;
|
|
169
|
+
this.age = props.age as number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Deserializes input to an instance of this class.
|
|
174
|
+
* Automatically detects whether input is a JSON string or object.
|
|
175
|
+
* @param input - JSON string or object to deserialize
|
|
176
|
+
* @param opts - Optional deserialization options
|
|
177
|
+
* @returns Result containing the deserialized instance or validation errors
|
|
178
|
+
*/
|
|
179
|
+
static deserialize(
|
|
180
|
+
input: unknown,
|
|
181
|
+
opts?: DeserializeOptions
|
|
182
|
+
): Result<
|
|
183
|
+
User,
|
|
184
|
+
Array<{
|
|
185
|
+
field: string;
|
|
186
|
+
message: string;
|
|
187
|
+
}>
|
|
188
|
+
> {
|
|
189
|
+
try {
|
|
190
|
+
// Auto-detect: if string, parse as JSON first
|
|
191
|
+
const data = typeof input === 'string' ? JSON.parse(input) : input;
|
|
192
|
+
|
|
193
|
+
const ctx = DeserializeContext.create();
|
|
194
|
+
const resultOrRef = User.deserializeWithContext(data, ctx);
|
|
195
|
+
if (PendingRef.is(resultOrRef)) {
|
|
196
|
+
return Result.err([
|
|
197
|
+
{
|
|
198
|
+
field: '_root',
|
|
199
|
+
message: 'User.deserialize: root cannot be a forward reference'
|
|
200
|
+
}
|
|
201
|
+
]);
|
|
202
|
+
}
|
|
203
|
+
ctx.applyPatches();
|
|
204
|
+
if (opts?.freeze) {
|
|
205
|
+
ctx.freezeAll();
|
|
206
|
+
}
|
|
207
|
+
return Result.ok(resultOrRef);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
if (e instanceof DeserializeError) {
|
|
210
|
+
return Result.err(e.errors);
|
|
211
|
+
}
|
|
212
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
213
|
+
return Result.err([
|
|
214
|
+
{
|
|
215
|
+
field: '_root',
|
|
216
|
+
message
|
|
217
|
+
}
|
|
218
|
+
]);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** @internal */
|
|
223
|
+
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
|
|
224
|
+
if (value?.__ref !== undefined) {
|
|
225
|
+
return ctx.getOrDefer(value.__ref);
|
|
226
|
+
}
|
|
227
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
228
|
+
throw new DeserializeError([
|
|
229
|
+
{
|
|
230
|
+
field: '_root',
|
|
231
|
+
message: 'User.deserializeWithContext: expected an object'
|
|
232
|
+
}
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
const obj = value as Record<string, unknown>;
|
|
236
|
+
const errors: Array<{
|
|
237
|
+
field: string;
|
|
238
|
+
message: string;
|
|
239
|
+
}> = [];
|
|
240
|
+
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
|
|
241
|
+
for (const key of Object.keys(obj)) {
|
|
242
|
+
if (!knownKeys.has(key)) {
|
|
243
|
+
errors.push({
|
|
244
|
+
field: key,
|
|
245
|
+
message: 'unknown field'
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (!('id' in obj)) {
|
|
250
|
+
errors.push({
|
|
251
|
+
field: 'id',
|
|
252
|
+
message: 'missing required field'
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (!('email' in obj)) {
|
|
256
|
+
errors.push({
|
|
257
|
+
field: 'email',
|
|
258
|
+
message: 'missing required field'
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (errors.length > 0) {
|
|
262
|
+
throw new DeserializeError(errors);
|
|
263
|
+
}
|
|
264
|
+
const instance = Object.create(User.prototype) as User;
|
|
265
|
+
if (obj.__id !== undefined) {
|
|
266
|
+
ctx.register(obj.__id as number, instance);
|
|
267
|
+
}
|
|
268
|
+
ctx.trackForFreeze(instance);
|
|
269
|
+
{
|
|
270
|
+
const __raw_id = obj['id'] as number;
|
|
271
|
+
instance.id = __raw_id;
|
|
272
|
+
}
|
|
273
|
+
{
|
|
274
|
+
const __raw_email = obj['email'] as string;
|
|
275
|
+
instance.email = __raw_email;
|
|
276
|
+
}
|
|
277
|
+
if ('name' in obj && obj['name'] !== undefined) {
|
|
278
|
+
const __raw_name = obj['name'] as string;
|
|
279
|
+
instance.name = __raw_name;
|
|
280
|
+
} else {
|
|
281
|
+
instance.name = "guest";
|
|
282
|
+
}
|
|
283
|
+
if ('age' in obj && obj['age'] !== undefined) {
|
|
284
|
+
const __raw_age = obj['age'] as number;
|
|
285
|
+
instance.age = __raw_age;
|
|
286
|
+
}
|
|
287
|
+
if (errors.length > 0) {
|
|
288
|
+
throw new DeserializeError(errors);
|
|
289
|
+
}
|
|
290
|
+
return instance;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
static validateField<K extends keyof User>(
|
|
294
|
+
field: K,
|
|
295
|
+
value: User[K]
|
|
296
|
+
): Array<{
|
|
297
|
+
field: string;
|
|
298
|
+
message: string;
|
|
299
|
+
}> {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
static validateFields(partial: Partial<User>): Array<{
|
|
304
|
+
field: string;
|
|
305
|
+
message: string;
|
|
306
|
+
}> {
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
static hasShape(obj: unknown): boolean {
|
|
311
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
const o = obj as Record<string, unknown>;
|
|
315
|
+
return 'id' in o && 'email' in o;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
static is(obj: unknown): obj is User {
|
|
319
|
+
if (obj instanceof User) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
if (!User.hasShape(obj)) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
const result = User.deserialize(obj);
|
|
326
|
+
return Result.isOk(result);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Usage:
|
|
331
|
+
const result = User.deserialize('{"id":1,"email":"test@example.com"}');
|
|
332
|
+
if (Result.isOk(result)) {
|
|
333
|
+
const user = result.value;
|
|
334
|
+
} else {
|
|
335
|
+
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Generated output:
|
|
340
|
+
|
|
108
341
|
```typescript
|
|
109
|
-
|
|
110
|
-
|
|
342
|
+
import { DeserializeContext } from 'macroforge/serde';
|
|
343
|
+
import { DeserializeError } from 'macroforge/serde';
|
|
344
|
+
import type { DeserializeOptions } from 'macroforge/serde';
|
|
345
|
+
import { PendingRef } from 'macroforge/serde';
|
|
346
|
+
|
|
347
|
+
/** @serde({ denyUnknownFields: true }) */
|
|
111
348
|
class User {
|
|
112
349
|
id: number;
|
|
113
350
|
|
|
114
|
-
@serde(validate(email, maxLength(255)))
|
|
115
351
|
email: string;
|
|
116
352
|
|
|
117
|
-
@serde(default = "guest")
|
|
118
353
|
name: string;
|
|
119
354
|
|
|
120
|
-
@serde(validate(positive))
|
|
121
355
|
age?: number;
|
|
356
|
+
|
|
357
|
+
constructor(props: {
|
|
358
|
+
id: number;
|
|
359
|
+
email: string;
|
|
360
|
+
name?: string;
|
|
361
|
+
age?: number;
|
|
362
|
+
}) {
|
|
363
|
+
this.id = props.id;
|
|
364
|
+
this.email = props.email;
|
|
365
|
+
this.name = props.name as string;
|
|
366
|
+
this.age = props.age as number;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Deserializes input to an instance of this class.
|
|
371
|
+
* Automatically detects whether input is a JSON string or object.
|
|
372
|
+
* @param input - JSON string or object to deserialize
|
|
373
|
+
* @param opts - Optional deserialization options
|
|
374
|
+
* @returns Result containing the deserialized instance or validation errors
|
|
375
|
+
*/
|
|
376
|
+
static deserialize(
|
|
377
|
+
input: unknown,
|
|
378
|
+
opts?: DeserializeOptions
|
|
379
|
+
): Result<
|
|
380
|
+
User,
|
|
381
|
+
Array<{
|
|
382
|
+
field: string;
|
|
383
|
+
message: string;
|
|
384
|
+
}>
|
|
385
|
+
> {
|
|
386
|
+
try {
|
|
387
|
+
// Auto-detect: if string, parse as JSON first
|
|
388
|
+
const data = typeof input === 'string' ? JSON.parse(input) : input;
|
|
389
|
+
|
|
390
|
+
const ctx = DeserializeContext.create();
|
|
391
|
+
const resultOrRef = User.deserializeWithContext(data, ctx);
|
|
392
|
+
if (PendingRef.is(resultOrRef)) {
|
|
393
|
+
return Result.err([
|
|
394
|
+
{
|
|
395
|
+
field: '_root',
|
|
396
|
+
message: 'User.deserialize: root cannot be a forward reference'
|
|
397
|
+
}
|
|
398
|
+
]);
|
|
399
|
+
}
|
|
400
|
+
ctx.applyPatches();
|
|
401
|
+
if (opts?.freeze) {
|
|
402
|
+
ctx.freezeAll();
|
|
403
|
+
}
|
|
404
|
+
return Result.ok(resultOrRef);
|
|
405
|
+
} catch (e) {
|
|
406
|
+
if (e instanceof DeserializeError) {
|
|
407
|
+
return Result.err(e.errors);
|
|
408
|
+
}
|
|
409
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
410
|
+
return Result.err([
|
|
411
|
+
{
|
|
412
|
+
field: '_root',
|
|
413
|
+
message
|
|
414
|
+
}
|
|
415
|
+
]);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** @internal */
|
|
420
|
+
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
|
|
421
|
+
if (value?.__ref !== undefined) {
|
|
422
|
+
return ctx.getOrDefer(value.__ref);
|
|
423
|
+
}
|
|
424
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
425
|
+
throw new DeserializeError([
|
|
426
|
+
{
|
|
427
|
+
field: '_root',
|
|
428
|
+
message: 'User.deserializeWithContext: expected an object'
|
|
429
|
+
}
|
|
430
|
+
]);
|
|
431
|
+
}
|
|
432
|
+
const obj = value as Record<string, unknown>;
|
|
433
|
+
const errors: Array<{
|
|
434
|
+
field: string;
|
|
435
|
+
message: string;
|
|
436
|
+
}> = [];
|
|
437
|
+
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
|
|
438
|
+
for (const key of Object.keys(obj)) {
|
|
439
|
+
if (!knownKeys.has(key)) {
|
|
440
|
+
errors.push({
|
|
441
|
+
field: key,
|
|
442
|
+
message: 'unknown field'
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (!('id' in obj)) {
|
|
447
|
+
errors.push({
|
|
448
|
+
field: 'id',
|
|
449
|
+
message: 'missing required field'
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
if (!('email' in obj)) {
|
|
453
|
+
errors.push({
|
|
454
|
+
field: 'email',
|
|
455
|
+
message: 'missing required field'
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (errors.length > 0) {
|
|
459
|
+
throw new DeserializeError(errors);
|
|
460
|
+
}
|
|
461
|
+
const instance = Object.create(User.prototype) as User;
|
|
462
|
+
if (obj.__id !== undefined) {
|
|
463
|
+
ctx.register(obj.__id as number, instance);
|
|
464
|
+
}
|
|
465
|
+
ctx.trackForFreeze(instance);
|
|
466
|
+
{
|
|
467
|
+
const __raw_id = obj['id'] as number;
|
|
468
|
+
instance.id = __raw_id;
|
|
469
|
+
}
|
|
470
|
+
{
|
|
471
|
+
const __raw_email = obj['email'] as string;
|
|
472
|
+
instance.email = __raw_email;
|
|
473
|
+
}
|
|
474
|
+
if ('name' in obj && obj['name'] !== undefined) {
|
|
475
|
+
const __raw_name = obj['name'] as string;
|
|
476
|
+
instance.name = __raw_name;
|
|
477
|
+
} else {
|
|
478
|
+
instance.name = 'guest';
|
|
479
|
+
}
|
|
480
|
+
if ('age' in obj && obj['age'] !== undefined) {
|
|
481
|
+
const __raw_age = obj['age'] as number;
|
|
482
|
+
instance.age = __raw_age;
|
|
483
|
+
}
|
|
484
|
+
if (errors.length > 0) {
|
|
485
|
+
throw new DeserializeError(errors);
|
|
486
|
+
}
|
|
487
|
+
return instance;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
static validateField<K extends keyof User>(
|
|
491
|
+
field: K,
|
|
492
|
+
value: User[K]
|
|
493
|
+
): Array<{
|
|
494
|
+
field: string;
|
|
495
|
+
message: string;
|
|
496
|
+
}> {
|
|
497
|
+
return [];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
static validateFields(partial: Partial<User>): Array<{
|
|
501
|
+
field: string;
|
|
502
|
+
message: string;
|
|
503
|
+
}> {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
static hasShape(obj: unknown): boolean {
|
|
508
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
const o = obj as Record<string, unknown>;
|
|
512
|
+
return 'id' in o && 'email' in o;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
static is(obj: unknown): obj is User {
|
|
516
|
+
if (obj instanceof User) {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
if (!User.hasShape(obj)) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
const result = User.deserialize(obj);
|
|
523
|
+
return Result.isOk(result);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Usage:
|
|
528
|
+
const result = User.deserialize('{"id":1,"email":"test@example.com"}');
|
|
529
|
+
if (Result.isOk(result)) {
|
|
530
|
+
const user = result.value;
|
|
531
|
+
} else {
|
|
532
|
+
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Generated output:
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
import { DeserializeContext } from 'macroforge/serde';
|
|
540
|
+
import { DeserializeError } from 'macroforge/serde';
|
|
541
|
+
import type { DeserializeOptions } from 'macroforge/serde';
|
|
542
|
+
import { PendingRef } from 'macroforge/serde';
|
|
543
|
+
|
|
544
|
+
/** @serde({ denyUnknownFields: true }) */
|
|
545
|
+
class User {
|
|
546
|
+
id: number;
|
|
547
|
+
|
|
548
|
+
email: string;
|
|
549
|
+
|
|
550
|
+
name: string;
|
|
551
|
+
|
|
552
|
+
age?: number;
|
|
553
|
+
|
|
554
|
+
constructor(props: {
|
|
555
|
+
id: number;
|
|
556
|
+
email: string;
|
|
557
|
+
name?: string;
|
|
558
|
+
age?: number;
|
|
559
|
+
}) {
|
|
560
|
+
this.id = props.id;
|
|
561
|
+
this.email = props.email;
|
|
562
|
+
this.name = props.name as string;
|
|
563
|
+
this.age = props.age as number;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Deserializes input to an instance of this class.
|
|
568
|
+
* Automatically detects whether input is a JSON string or object.
|
|
569
|
+
* @param input - JSON string or object to deserialize
|
|
570
|
+
* @param opts - Optional deserialization options
|
|
571
|
+
* @returns Result containing the deserialized instance or validation errors
|
|
572
|
+
*/
|
|
573
|
+
static deserialize(
|
|
574
|
+
input: unknown,
|
|
575
|
+
opts?: DeserializeOptions
|
|
576
|
+
): Result<
|
|
577
|
+
User,
|
|
578
|
+
Array<{
|
|
579
|
+
field: string;
|
|
580
|
+
message: string;
|
|
581
|
+
}>
|
|
582
|
+
> {
|
|
583
|
+
try {
|
|
584
|
+
// Auto-detect: if string, parse as JSON first
|
|
585
|
+
const data = typeof input === 'string' ? JSON.parse(input) : input;
|
|
586
|
+
|
|
587
|
+
const ctx = DeserializeContext.create();
|
|
588
|
+
const resultOrRef = User.deserializeWithContext(data, ctx);
|
|
589
|
+
if (PendingRef.is(resultOrRef)) {
|
|
590
|
+
return Result.err([
|
|
591
|
+
{
|
|
592
|
+
field: '_root',
|
|
593
|
+
message: 'User.deserialize: root cannot be a forward reference'
|
|
594
|
+
}
|
|
595
|
+
]);
|
|
596
|
+
}
|
|
597
|
+
ctx.applyPatches();
|
|
598
|
+
if (opts?.freeze) {
|
|
599
|
+
ctx.freezeAll();
|
|
600
|
+
}
|
|
601
|
+
return Result.ok(resultOrRef);
|
|
602
|
+
} catch (e) {
|
|
603
|
+
if (e instanceof DeserializeError) {
|
|
604
|
+
return Result.err(e.errors);
|
|
605
|
+
}
|
|
606
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
607
|
+
return Result.err([
|
|
608
|
+
{
|
|
609
|
+
field: '_root',
|
|
610
|
+
message
|
|
611
|
+
}
|
|
612
|
+
]);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** @internal */
|
|
617
|
+
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
|
|
618
|
+
if (value?.__ref !== undefined) {
|
|
619
|
+
return ctx.getOrDefer(value.__ref);
|
|
620
|
+
}
|
|
621
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
622
|
+
throw new DeserializeError([
|
|
623
|
+
{
|
|
624
|
+
field: '_root',
|
|
625
|
+
message: 'User.deserializeWithContext: expected an object'
|
|
626
|
+
}
|
|
627
|
+
]);
|
|
628
|
+
}
|
|
629
|
+
const obj = value as Record<string, unknown>;
|
|
630
|
+
const errors: Array<{
|
|
631
|
+
field: string;
|
|
632
|
+
message: string;
|
|
633
|
+
}> = [];
|
|
634
|
+
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
|
|
635
|
+
for (const key of Object.keys(obj)) {
|
|
636
|
+
if (!knownKeys.has(key)) {
|
|
637
|
+
errors.push({
|
|
638
|
+
field: key,
|
|
639
|
+
message: 'unknown field'
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (!('id' in obj)) {
|
|
644
|
+
errors.push({
|
|
645
|
+
field: 'id',
|
|
646
|
+
message: 'missing required field'
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
if (!('email' in obj)) {
|
|
650
|
+
errors.push({
|
|
651
|
+
field: 'email',
|
|
652
|
+
message: 'missing required field'
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
if (errors.length > 0) {
|
|
656
|
+
throw new DeserializeError(errors);
|
|
657
|
+
}
|
|
658
|
+
const instance = Object.create(User.prototype) as User;
|
|
659
|
+
if (obj.__id !== undefined) {
|
|
660
|
+
ctx.register(obj.__id as number, instance);
|
|
661
|
+
}
|
|
662
|
+
ctx.trackForFreeze(instance);
|
|
663
|
+
{
|
|
664
|
+
const __raw_id = obj['id'] as number;
|
|
665
|
+
instance.id = __raw_id;
|
|
666
|
+
}
|
|
667
|
+
{
|
|
668
|
+
const __raw_email = obj['email'] as string;
|
|
669
|
+
instance.email = __raw_email;
|
|
670
|
+
}
|
|
671
|
+
if ('name' in obj && obj['name'] !== undefined) {
|
|
672
|
+
const __raw_name = obj['name'] as string;
|
|
673
|
+
instance.name = __raw_name;
|
|
674
|
+
} else {
|
|
675
|
+
instance.name = "guest";
|
|
676
|
+
}
|
|
677
|
+
if ('age' in obj && obj['age'] !== undefined) {
|
|
678
|
+
const __raw_age = obj['age'] as number;
|
|
679
|
+
instance.age = __raw_age;
|
|
680
|
+
}
|
|
681
|
+
if (errors.length > 0) {
|
|
682
|
+
throw new DeserializeError(errors);
|
|
683
|
+
}
|
|
684
|
+
return instance;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
static validateField<K extends keyof User>(
|
|
688
|
+
field: K,
|
|
689
|
+
value: User[K]
|
|
690
|
+
): Array<{
|
|
691
|
+
field: string;
|
|
692
|
+
message: string;
|
|
693
|
+
}> {
|
|
694
|
+
return [];
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
static validateFields(partial: Partial<User>): Array<{
|
|
698
|
+
field: string;
|
|
699
|
+
message: string;
|
|
700
|
+
}> {
|
|
701
|
+
return [];
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
static hasShape(obj: unknown): boolean {
|
|
705
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
const o = obj as Record<string, unknown>;
|
|
709
|
+
return 'id' in o && 'email' in o;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
static is(obj: unknown): obj is User {
|
|
713
|
+
if (obj instanceof User) {
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
if (!User.hasShape(obj)) {
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
const result = User.deserialize(obj);
|
|
720
|
+
return Result.isOk(result);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Usage:
|
|
725
|
+
const result = User.deserialize('{"id":1,"email":"test@example.com"}');
|
|
726
|
+
if (Result.isOk(result)) {
|
|
727
|
+
const user = result.value;
|
|
728
|
+
} else {
|
|
729
|
+
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
|
|
730
|
+
}
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
Generated output:
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
import { DeserializeContext } from 'macroforge/serde';
|
|
737
|
+
import { DeserializeError } from 'macroforge/serde';
|
|
738
|
+
import type { DeserializeOptions } from 'macroforge/serde';
|
|
739
|
+
import { PendingRef } from 'macroforge/serde';
|
|
740
|
+
|
|
741
|
+
/** @serde({ denyUnknownFields: true }) */
|
|
742
|
+
class User {
|
|
743
|
+
id: number;
|
|
744
|
+
|
|
745
|
+
email: string;
|
|
746
|
+
|
|
747
|
+
name: string;
|
|
748
|
+
|
|
749
|
+
age?: number;
|
|
750
|
+
|
|
751
|
+
constructor(props: {
|
|
752
|
+
id: number;
|
|
753
|
+
email: string;
|
|
754
|
+
name?: string;
|
|
755
|
+
age?: number;
|
|
756
|
+
}) {
|
|
757
|
+
this.id = props.id;
|
|
758
|
+
this.email = props.email;
|
|
759
|
+
this.name = props.name as string;
|
|
760
|
+
this.age = props.age as number;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Deserializes input to an instance of this class.
|
|
765
|
+
* Automatically detects whether input is a JSON string or object.
|
|
766
|
+
* @param input - JSON string or object to deserialize
|
|
767
|
+
* @param opts - Optional deserialization options
|
|
768
|
+
* @returns Result containing the deserialized instance or validation errors
|
|
769
|
+
*/
|
|
770
|
+
static deserialize(
|
|
771
|
+
input: unknown,
|
|
772
|
+
opts?: DeserializeOptions
|
|
773
|
+
): Result<
|
|
774
|
+
User,
|
|
775
|
+
Array<{
|
|
776
|
+
field: string;
|
|
777
|
+
message: string;
|
|
778
|
+
}>
|
|
779
|
+
> {
|
|
780
|
+
try {
|
|
781
|
+
// Auto-detect: if string, parse as JSON first
|
|
782
|
+
const data = typeof input === 'string' ? JSON.parse(input) : input;
|
|
783
|
+
|
|
784
|
+
const ctx = DeserializeContext.create();
|
|
785
|
+
const resultOrRef = User.deserializeWithContext(data, ctx);
|
|
786
|
+
if (PendingRef.is(resultOrRef)) {
|
|
787
|
+
return Result.err([
|
|
788
|
+
{
|
|
789
|
+
field: '_root',
|
|
790
|
+
message: 'User.deserialize: root cannot be a forward reference'
|
|
791
|
+
}
|
|
792
|
+
]);
|
|
793
|
+
}
|
|
794
|
+
ctx.applyPatches();
|
|
795
|
+
if (opts?.freeze) {
|
|
796
|
+
ctx.freezeAll();
|
|
797
|
+
}
|
|
798
|
+
return Result.ok(resultOrRef);
|
|
799
|
+
} catch (e) {
|
|
800
|
+
if (e instanceof DeserializeError) {
|
|
801
|
+
return Result.err(e.errors);
|
|
802
|
+
}
|
|
803
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
804
|
+
return Result.err([
|
|
805
|
+
{
|
|
806
|
+
field: '_root',
|
|
807
|
+
message
|
|
808
|
+
}
|
|
809
|
+
]);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** @internal */
|
|
814
|
+
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
|
|
815
|
+
if (value?.__ref !== undefined) {
|
|
816
|
+
return ctx.getOrDefer(value.__ref);
|
|
817
|
+
}
|
|
818
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
819
|
+
throw new DeserializeError([
|
|
820
|
+
{
|
|
821
|
+
field: '_root',
|
|
822
|
+
message: 'User.deserializeWithContext: expected an object'
|
|
823
|
+
}
|
|
824
|
+
]);
|
|
825
|
+
}
|
|
826
|
+
const obj = value as Record<string, unknown>;
|
|
827
|
+
const errors: Array<{
|
|
828
|
+
field: string;
|
|
829
|
+
message: string;
|
|
830
|
+
}> = [];
|
|
831
|
+
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
|
|
832
|
+
for (const key of Object.keys(obj)) {
|
|
833
|
+
if (!knownKeys.has(key)) {
|
|
834
|
+
errors.push({
|
|
835
|
+
field: key,
|
|
836
|
+
message: 'unknown field'
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (!('id' in obj)) {
|
|
841
|
+
errors.push({
|
|
842
|
+
field: 'id',
|
|
843
|
+
message: 'missing required field'
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
if (!('email' in obj)) {
|
|
847
|
+
errors.push({
|
|
848
|
+
field: 'email',
|
|
849
|
+
message: 'missing required field'
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
if (errors.length > 0) {
|
|
853
|
+
throw new DeserializeError(errors);
|
|
854
|
+
}
|
|
855
|
+
const instance = Object.create(User.prototype) as User;
|
|
856
|
+
if (obj.__id !== undefined) {
|
|
857
|
+
ctx.register(obj.__id as number, instance);
|
|
858
|
+
}
|
|
859
|
+
ctx.trackForFreeze(instance);
|
|
860
|
+
{
|
|
861
|
+
const __raw_id = obj['id'] as number;
|
|
862
|
+
instance.id = __raw_id;
|
|
863
|
+
}
|
|
864
|
+
{
|
|
865
|
+
const __raw_email = obj['email'] as string;
|
|
866
|
+
instance.email = __raw_email;
|
|
867
|
+
}
|
|
868
|
+
if ('name' in obj && obj['name'] !== undefined) {
|
|
869
|
+
const __raw_name = obj['name'] as string;
|
|
870
|
+
instance.name = __raw_name;
|
|
871
|
+
} else {
|
|
872
|
+
instance.name = 'guest';
|
|
873
|
+
}
|
|
874
|
+
if ('age' in obj && obj['age'] !== undefined) {
|
|
875
|
+
const __raw_age = obj['age'] as number;
|
|
876
|
+
instance.age = __raw_age;
|
|
877
|
+
}
|
|
878
|
+
if (errors.length > 0) {
|
|
879
|
+
throw new DeserializeError(errors);
|
|
880
|
+
}
|
|
881
|
+
return instance;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
static validateField<K extends keyof User>(
|
|
885
|
+
field: K,
|
|
886
|
+
value: User[K]
|
|
887
|
+
): Array<{
|
|
888
|
+
field: string;
|
|
889
|
+
message: string;
|
|
890
|
+
}> {
|
|
891
|
+
return [];
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
static validateFields(partial: Partial<User>): Array<{
|
|
895
|
+
field: string;
|
|
896
|
+
message: string;
|
|
897
|
+
}> {
|
|
898
|
+
return [];
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
static hasShape(obj: unknown): boolean {
|
|
902
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
const o = obj as Record<string, unknown>;
|
|
906
|
+
return 'id' in o && 'email' in o;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
static is(obj: unknown): obj is User {
|
|
910
|
+
if (obj instanceof User) {
|
|
911
|
+
return true;
|
|
912
|
+
}
|
|
913
|
+
if (!User.hasShape(obj)) {
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
const result = User.deserialize(obj);
|
|
917
|
+
return Result.isOk(result);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Usage:
|
|
922
|
+
const result = User.deserialize('{"id":1,"email":"test@example.com"}');
|
|
923
|
+
if (Result.isOk(result)) {
|
|
924
|
+
const user = result.value;
|
|
925
|
+
} else {
|
|
926
|
+
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
|
|
927
|
+
}
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
Generated output:
|
|
931
|
+
|
|
932
|
+
```typescript
|
|
933
|
+
import { DeserializeContext } from 'macroforge/serde';
|
|
934
|
+
import { DeserializeError } from 'macroforge/serde';
|
|
935
|
+
import type { DeserializeOptions } from 'macroforge/serde';
|
|
936
|
+
import { PendingRef } from 'macroforge/serde';
|
|
937
|
+
|
|
938
|
+
/** @serde({ denyUnknownFields: true }) */
|
|
939
|
+
class User {
|
|
940
|
+
id: number;
|
|
941
|
+
|
|
942
|
+
email: string;
|
|
943
|
+
|
|
944
|
+
name: string;
|
|
945
|
+
|
|
946
|
+
age?: number;
|
|
947
|
+
|
|
948
|
+
constructor(props: {
|
|
949
|
+
id: number;
|
|
950
|
+
email: string;
|
|
951
|
+
name?: string;
|
|
952
|
+
age?: number;
|
|
953
|
+
}) {
|
|
954
|
+
this.id = props.id;
|
|
955
|
+
this.email = props.email;
|
|
956
|
+
this.name = props.name as string;
|
|
957
|
+
this.age = props.age as number;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Deserializes input to an instance of this class.
|
|
962
|
+
* Automatically detects whether input is a JSON string or object.
|
|
963
|
+
* @param input - JSON string or object to deserialize
|
|
964
|
+
* @param opts - Optional deserialization options
|
|
965
|
+
* @returns Result containing the deserialized instance or validation errors
|
|
966
|
+
*/
|
|
967
|
+
static deserialize(
|
|
968
|
+
input: unknown,
|
|
969
|
+
opts?: DeserializeOptions
|
|
970
|
+
): Result<
|
|
971
|
+
User,
|
|
972
|
+
Array<{
|
|
973
|
+
field: string;
|
|
974
|
+
message: string;
|
|
975
|
+
}>
|
|
976
|
+
> {
|
|
977
|
+
try {
|
|
978
|
+
// Auto-detect: if string, parse as JSON first
|
|
979
|
+
const data = typeof input === 'string' ? JSON.parse(input) : input;
|
|
980
|
+
|
|
981
|
+
const ctx = DeserializeContext.create();
|
|
982
|
+
const resultOrRef = User.deserializeWithContext(data, ctx);
|
|
983
|
+
if (PendingRef.is(resultOrRef)) {
|
|
984
|
+
return Result.err([
|
|
985
|
+
{
|
|
986
|
+
field: '_root',
|
|
987
|
+
message: 'User.deserialize: root cannot be a forward reference'
|
|
988
|
+
}
|
|
989
|
+
]);
|
|
990
|
+
}
|
|
991
|
+
ctx.applyPatches();
|
|
992
|
+
if (opts?.freeze) {
|
|
993
|
+
ctx.freezeAll();
|
|
994
|
+
}
|
|
995
|
+
return Result.ok(resultOrRef);
|
|
996
|
+
} catch (e) {
|
|
997
|
+
if (e instanceof DeserializeError) {
|
|
998
|
+
return Result.err(e.errors);
|
|
999
|
+
}
|
|
1000
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1001
|
+
return Result.err([
|
|
1002
|
+
{
|
|
1003
|
+
field: '_root',
|
|
1004
|
+
message
|
|
1005
|
+
}
|
|
1006
|
+
]);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/** @internal */
|
|
1011
|
+
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
|
|
1012
|
+
if (value?.__ref !== undefined) {
|
|
1013
|
+
return ctx.getOrDefer(value.__ref);
|
|
1014
|
+
}
|
|
1015
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1016
|
+
throw new DeserializeError([
|
|
1017
|
+
{
|
|
1018
|
+
field: '_root',
|
|
1019
|
+
message: 'User.deserializeWithContext: expected an object'
|
|
1020
|
+
}
|
|
1021
|
+
]);
|
|
1022
|
+
}
|
|
1023
|
+
const obj = value as Record<string, unknown>;
|
|
1024
|
+
const errors: Array<{
|
|
1025
|
+
field: string;
|
|
1026
|
+
message: string;
|
|
1027
|
+
}> = [];
|
|
1028
|
+
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
|
|
1029
|
+
for (const key of Object.keys(obj)) {
|
|
1030
|
+
if (!knownKeys.has(key)) {
|
|
1031
|
+
errors.push({
|
|
1032
|
+
field: key,
|
|
1033
|
+
message: 'unknown field'
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (!('id' in obj)) {
|
|
1038
|
+
errors.push({
|
|
1039
|
+
field: 'id',
|
|
1040
|
+
message: 'missing required field'
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
if (!('email' in obj)) {
|
|
1044
|
+
errors.push({
|
|
1045
|
+
field: 'email',
|
|
1046
|
+
message: 'missing required field'
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
if (errors.length > 0) {
|
|
1050
|
+
throw new DeserializeError(errors);
|
|
1051
|
+
}
|
|
1052
|
+
const instance = Object.create(User.prototype) as User;
|
|
1053
|
+
if (obj.__id !== undefined) {
|
|
1054
|
+
ctx.register(obj.__id as number, instance);
|
|
1055
|
+
}
|
|
1056
|
+
ctx.trackForFreeze(instance);
|
|
1057
|
+
{
|
|
1058
|
+
const __raw_id = obj['id'] as number;
|
|
1059
|
+
instance.id = __raw_id;
|
|
1060
|
+
}
|
|
1061
|
+
{
|
|
1062
|
+
const __raw_email = obj['email'] as string;
|
|
1063
|
+
instance.email = __raw_email;
|
|
1064
|
+
}
|
|
1065
|
+
if ('name' in obj && obj['name'] !== undefined) {
|
|
1066
|
+
const __raw_name = obj['name'] as string;
|
|
1067
|
+
instance.name = __raw_name;
|
|
1068
|
+
} else {
|
|
1069
|
+
instance.name = "guest";
|
|
1070
|
+
}
|
|
1071
|
+
if ('age' in obj && obj['age'] !== undefined) {
|
|
1072
|
+
const __raw_age = obj['age'] as number;
|
|
1073
|
+
instance.age = __raw_age;
|
|
1074
|
+
}
|
|
1075
|
+
if (errors.length > 0) {
|
|
1076
|
+
throw new DeserializeError(errors);
|
|
1077
|
+
}
|
|
1078
|
+
return instance;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
static validateField<K extends keyof User>(
|
|
1082
|
+
field: K,
|
|
1083
|
+
value: User[K]
|
|
1084
|
+
): Array<{
|
|
1085
|
+
field: string;
|
|
1086
|
+
message: string;
|
|
1087
|
+
}> {
|
|
1088
|
+
return [];
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
static validateFields(partial: Partial<User>): Array<{
|
|
1092
|
+
field: string;
|
|
1093
|
+
message: string;
|
|
1094
|
+
}> {
|
|
1095
|
+
return [];
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
static hasShape(obj: unknown): boolean {
|
|
1099
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
const o = obj as Record<string, unknown>;
|
|
1103
|
+
return 'id' in o && 'email' in o;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
static is(obj: unknown): obj is User {
|
|
1107
|
+
if (obj instanceof User) {
|
|
1108
|
+
return true;
|
|
1109
|
+
}
|
|
1110
|
+
if (!User.hasShape(obj)) {
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
const result = User.deserialize(obj);
|
|
1114
|
+
return Result.isOk(result);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Usage:
|
|
1119
|
+
const result = User.deserialize('{"id":1,"email":"test@example.com"}');
|
|
1120
|
+
if (Result.isOk(result)) {
|
|
1121
|
+
const user = result.value;
|
|
1122
|
+
} else {
|
|
1123
|
+
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
|
|
1124
|
+
}
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
Generated output:
|
|
1128
|
+
|
|
1129
|
+
```typescript
|
|
1130
|
+
import { DeserializeContext } from 'macroforge/serde';
|
|
1131
|
+
import { DeserializeError } from 'macroforge/serde';
|
|
1132
|
+
import type { DeserializeOptions } from 'macroforge/serde';
|
|
1133
|
+
import { PendingRef } from 'macroforge/serde';
|
|
1134
|
+
|
|
1135
|
+
/** @serde({ denyUnknownFields: true }) */
|
|
1136
|
+
class User {
|
|
1137
|
+
id: number;
|
|
1138
|
+
|
|
1139
|
+
email: string;
|
|
1140
|
+
|
|
1141
|
+
name: string;
|
|
1142
|
+
|
|
1143
|
+
age?: number;
|
|
1144
|
+
|
|
1145
|
+
constructor(props: {
|
|
1146
|
+
id: number;
|
|
1147
|
+
email: string;
|
|
1148
|
+
name?: string;
|
|
1149
|
+
age?: number;
|
|
1150
|
+
}) {
|
|
1151
|
+
this.id = props.id;
|
|
1152
|
+
this.email = props.email;
|
|
1153
|
+
this.name = props.name as string;
|
|
1154
|
+
this.age = props.age as number;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Deserializes input to an instance of this class.
|
|
1159
|
+
* Automatically detects whether input is a JSON string or object.
|
|
1160
|
+
* @param input - JSON string or object to deserialize
|
|
1161
|
+
* @param opts - Optional deserialization options
|
|
1162
|
+
* @returns Result containing the deserialized instance or validation errors
|
|
1163
|
+
*/
|
|
1164
|
+
static deserialize(
|
|
1165
|
+
input: unknown,
|
|
1166
|
+
opts?: DeserializeOptions
|
|
1167
|
+
): Result<
|
|
1168
|
+
User,
|
|
1169
|
+
Array<{
|
|
1170
|
+
field: string;
|
|
1171
|
+
message: string;
|
|
1172
|
+
}>
|
|
1173
|
+
> {
|
|
1174
|
+
try {
|
|
1175
|
+
// Auto-detect: if string, parse as JSON first
|
|
1176
|
+
const data = typeof input === 'string' ? JSON.parse(input) : input;
|
|
1177
|
+
|
|
1178
|
+
const ctx = DeserializeContext.create();
|
|
1179
|
+
const resultOrRef = User.deserializeWithContext(data, ctx);
|
|
1180
|
+
if (PendingRef.is(resultOrRef)) {
|
|
1181
|
+
return Result.err([
|
|
1182
|
+
{
|
|
1183
|
+
field: '_root',
|
|
1184
|
+
message: 'User.deserialize: root cannot be a forward reference'
|
|
1185
|
+
}
|
|
1186
|
+
]);
|
|
1187
|
+
}
|
|
1188
|
+
ctx.applyPatches();
|
|
1189
|
+
if (opts?.freeze) {
|
|
1190
|
+
ctx.freezeAll();
|
|
1191
|
+
}
|
|
1192
|
+
return Result.ok(resultOrRef);
|
|
1193
|
+
} catch (e) {
|
|
1194
|
+
if (e instanceof DeserializeError) {
|
|
1195
|
+
return Result.err(e.errors);
|
|
1196
|
+
}
|
|
1197
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1198
|
+
return Result.err([
|
|
1199
|
+
{
|
|
1200
|
+
field: '_root',
|
|
1201
|
+
message
|
|
1202
|
+
}
|
|
1203
|
+
]);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/** @internal */
|
|
1208
|
+
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
|
|
1209
|
+
if (value?.__ref !== undefined) {
|
|
1210
|
+
return ctx.getOrDefer(value.__ref);
|
|
1211
|
+
}
|
|
1212
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1213
|
+
throw new DeserializeError([
|
|
1214
|
+
{
|
|
1215
|
+
field: '_root',
|
|
1216
|
+
message: 'User.deserializeWithContext: expected an object'
|
|
1217
|
+
}
|
|
1218
|
+
]);
|
|
1219
|
+
}
|
|
1220
|
+
const obj = value as Record<string, unknown>;
|
|
1221
|
+
const errors: Array<{
|
|
1222
|
+
field: string;
|
|
1223
|
+
message: string;
|
|
1224
|
+
}> = [];
|
|
1225
|
+
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
|
|
1226
|
+
for (const key of Object.keys(obj)) {
|
|
1227
|
+
if (!knownKeys.has(key)) {
|
|
1228
|
+
errors.push({
|
|
1229
|
+
field: key,
|
|
1230
|
+
message: 'unknown field'
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (!('id' in obj)) {
|
|
1235
|
+
errors.push({
|
|
1236
|
+
field: 'id',
|
|
1237
|
+
message: 'missing required field'
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
if (!('email' in obj)) {
|
|
1241
|
+
errors.push({
|
|
1242
|
+
field: 'email',
|
|
1243
|
+
message: 'missing required field'
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
if (errors.length > 0) {
|
|
1247
|
+
throw new DeserializeError(errors);
|
|
1248
|
+
}
|
|
1249
|
+
const instance = Object.create(User.prototype) as User;
|
|
1250
|
+
if (obj.__id !== undefined) {
|
|
1251
|
+
ctx.register(obj.__id as number, instance);
|
|
1252
|
+
}
|
|
1253
|
+
ctx.trackForFreeze(instance);
|
|
1254
|
+
{
|
|
1255
|
+
const __raw_id = obj['id'] as number;
|
|
1256
|
+
instance.id = __raw_id;
|
|
1257
|
+
}
|
|
1258
|
+
{
|
|
1259
|
+
const __raw_email = obj['email'] as string;
|
|
1260
|
+
instance.email = __raw_email;
|
|
1261
|
+
}
|
|
1262
|
+
if ('name' in obj && obj['name'] !== undefined) {
|
|
1263
|
+
const __raw_name = obj['name'] as string;
|
|
1264
|
+
instance.name = __raw_name;
|
|
1265
|
+
} else {
|
|
1266
|
+
instance.name = 'guest';
|
|
1267
|
+
}
|
|
1268
|
+
if ('age' in obj && obj['age'] !== undefined) {
|
|
1269
|
+
const __raw_age = obj['age'] as number;
|
|
1270
|
+
instance.age = __raw_age;
|
|
1271
|
+
}
|
|
1272
|
+
if (errors.length > 0) {
|
|
1273
|
+
throw new DeserializeError(errors);
|
|
1274
|
+
}
|
|
1275
|
+
return instance;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
static validateField<K extends keyof User>(
|
|
1279
|
+
field: K,
|
|
1280
|
+
value: User[K]
|
|
1281
|
+
): Array<{
|
|
1282
|
+
field: string;
|
|
1283
|
+
message: string;
|
|
1284
|
+
}> {
|
|
1285
|
+
return [];
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
static validateFields(partial: Partial<User>): Array<{
|
|
1289
|
+
field: string;
|
|
1290
|
+
message: string;
|
|
1291
|
+
}> {
|
|
1292
|
+
return [];
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
static hasShape(obj: unknown): boolean {
|
|
1296
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
const o = obj as Record<string, unknown>;
|
|
1300
|
+
return 'id' in o && 'email' in o;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
static is(obj: unknown): obj is User {
|
|
1304
|
+
if (obj instanceof User) {
|
|
1305
|
+
return true;
|
|
1306
|
+
}
|
|
1307
|
+
if (!User.hasShape(obj)) {
|
|
1308
|
+
return false;
|
|
1309
|
+
}
|
|
1310
|
+
const result = User.deserialize(obj);
|
|
1311
|
+
return Result.isOk(result);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Usage:
|
|
1316
|
+
const result = User.deserialize('{"id":1,"email":"test@example.com"}');
|
|
1317
|
+
if (Result.isOk(result)) {
|
|
1318
|
+
const user = result.value;
|
|
1319
|
+
} else {
|
|
1320
|
+
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
|
|
1321
|
+
}
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
Generated output:
|
|
1325
|
+
|
|
1326
|
+
```typescript
|
|
1327
|
+
import { DeserializeContext } from 'macroforge/serde';
|
|
1328
|
+
import { DeserializeError } from 'macroforge/serde';
|
|
1329
|
+
import type { DeserializeOptions } from 'macroforge/serde';
|
|
1330
|
+
import { PendingRef } from 'macroforge/serde';
|
|
1331
|
+
|
|
1332
|
+
/** @serde({ denyUnknownFields: true }) */
|
|
1333
|
+
class User {
|
|
1334
|
+
id: number;
|
|
1335
|
+
|
|
1336
|
+
email: string;
|
|
1337
|
+
|
|
1338
|
+
name: string;
|
|
1339
|
+
|
|
1340
|
+
age?: number;
|
|
1341
|
+
|
|
1342
|
+
constructor(props: {
|
|
1343
|
+
id: number;
|
|
1344
|
+
email: string;
|
|
1345
|
+
name?: string;
|
|
1346
|
+
age?: number;
|
|
1347
|
+
}) {
|
|
1348
|
+
this.id = props.id;
|
|
1349
|
+
this.email = props.email;
|
|
1350
|
+
this.name = props.name as string;
|
|
1351
|
+
this.age = props.age as number;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Deserializes input to an instance of this class.
|
|
1356
|
+
* Automatically detects whether input is a JSON string or object.
|
|
1357
|
+
* @param input - JSON string or object to deserialize
|
|
1358
|
+
* @param opts - Optional deserialization options
|
|
1359
|
+
* @returns Result containing the deserialized instance or validation errors
|
|
1360
|
+
*/
|
|
1361
|
+
static deserialize(
|
|
1362
|
+
input: unknown,
|
|
1363
|
+
opts?: DeserializeOptions
|
|
1364
|
+
): Result<
|
|
1365
|
+
User,
|
|
1366
|
+
Array<{
|
|
1367
|
+
field: string;
|
|
1368
|
+
message: string;
|
|
1369
|
+
}>
|
|
1370
|
+
> {
|
|
1371
|
+
try {
|
|
1372
|
+
// Auto-detect: if string, parse as JSON first
|
|
1373
|
+
const data = typeof input === 'string' ? JSON.parse(input) : input;
|
|
1374
|
+
|
|
1375
|
+
const ctx = DeserializeContext.create();
|
|
1376
|
+
const resultOrRef = User.deserializeWithContext(data, ctx);
|
|
1377
|
+
if (PendingRef.is(resultOrRef)) {
|
|
1378
|
+
return Result.err([
|
|
1379
|
+
{
|
|
1380
|
+
field: '_root',
|
|
1381
|
+
message: 'User.deserialize: root cannot be a forward reference'
|
|
1382
|
+
}
|
|
1383
|
+
]);
|
|
1384
|
+
}
|
|
1385
|
+
ctx.applyPatches();
|
|
1386
|
+
if (opts?.freeze) {
|
|
1387
|
+
ctx.freezeAll();
|
|
1388
|
+
}
|
|
1389
|
+
return Result.ok(resultOrRef);
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
if (e instanceof DeserializeError) {
|
|
1392
|
+
return Result.err(e.errors);
|
|
1393
|
+
}
|
|
1394
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1395
|
+
return Result.err([
|
|
1396
|
+
{
|
|
1397
|
+
field: '_root',
|
|
1398
|
+
message
|
|
1399
|
+
}
|
|
1400
|
+
]);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/** @internal */
|
|
1405
|
+
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
|
|
1406
|
+
if (value?.__ref !== undefined) {
|
|
1407
|
+
return ctx.getOrDefer(value.__ref);
|
|
1408
|
+
}
|
|
1409
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1410
|
+
throw new DeserializeError([
|
|
1411
|
+
{
|
|
1412
|
+
field: '_root',
|
|
1413
|
+
message: 'User.deserializeWithContext: expected an object'
|
|
1414
|
+
}
|
|
1415
|
+
]);
|
|
1416
|
+
}
|
|
1417
|
+
const obj = value as Record<string, unknown>;
|
|
1418
|
+
const errors: Array<{
|
|
1419
|
+
field: string;
|
|
1420
|
+
message: string;
|
|
1421
|
+
}> = [];
|
|
1422
|
+
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
|
|
1423
|
+
for (const key of Object.keys(obj)) {
|
|
1424
|
+
if (!knownKeys.has(key)) {
|
|
1425
|
+
errors.push({
|
|
1426
|
+
field: key,
|
|
1427
|
+
message: 'unknown field'
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (!('id' in obj)) {
|
|
1432
|
+
errors.push({
|
|
1433
|
+
field: 'id',
|
|
1434
|
+
message: 'missing required field'
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
if (!('email' in obj)) {
|
|
1438
|
+
errors.push({
|
|
1439
|
+
field: 'email',
|
|
1440
|
+
message: 'missing required field'
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
if (errors.length > 0) {
|
|
1444
|
+
throw new DeserializeError(errors);
|
|
1445
|
+
}
|
|
1446
|
+
const instance = Object.create(User.prototype) as User;
|
|
1447
|
+
if (obj.__id !== undefined) {
|
|
1448
|
+
ctx.register(obj.__id as number, instance);
|
|
1449
|
+
}
|
|
1450
|
+
ctx.trackForFreeze(instance);
|
|
1451
|
+
{
|
|
1452
|
+
const __raw_id = obj['id'] as number;
|
|
1453
|
+
instance.id = __raw_id;
|
|
1454
|
+
}
|
|
1455
|
+
{
|
|
1456
|
+
const __raw_email = obj['email'] as string;
|
|
1457
|
+
instance.email = __raw_email;
|
|
1458
|
+
}
|
|
1459
|
+
if ('name' in obj && obj['name'] !== undefined) {
|
|
1460
|
+
const __raw_name = obj['name'] as string;
|
|
1461
|
+
instance.name = __raw_name;
|
|
1462
|
+
} else {
|
|
1463
|
+
instance.name = 'guest';
|
|
1464
|
+
}
|
|
1465
|
+
if ('age' in obj && obj['age'] !== undefined) {
|
|
1466
|
+
const __raw_age = obj['age'] as number;
|
|
1467
|
+
instance.age = __raw_age;
|
|
1468
|
+
}
|
|
1469
|
+
if (errors.length > 0) {
|
|
1470
|
+
throw new DeserializeError(errors);
|
|
1471
|
+
}
|
|
1472
|
+
return instance;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
static validateField<K extends keyof User>(
|
|
1476
|
+
field: K,
|
|
1477
|
+
value: User[K]
|
|
1478
|
+
): Array<{
|
|
1479
|
+
field: string;
|
|
1480
|
+
message: string;
|
|
1481
|
+
}> {
|
|
1482
|
+
return [];
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
static validateFields(partial: Partial<User>): Array<{
|
|
1486
|
+
field: string;
|
|
1487
|
+
message: string;
|
|
1488
|
+
}> {
|
|
1489
|
+
return [];
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
static hasShape(obj: unknown): boolean {
|
|
1493
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
|
1494
|
+
return false;
|
|
1495
|
+
}
|
|
1496
|
+
const o = obj as Record<string, unknown>;
|
|
1497
|
+
return 'id' in o && 'email' in o;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
static is(obj: unknown): obj is User {
|
|
1501
|
+
if (obj instanceof User) {
|
|
1502
|
+
return true;
|
|
1503
|
+
}
|
|
1504
|
+
if (!User.hasShape(obj)) {
|
|
1505
|
+
return false;
|
|
1506
|
+
}
|
|
1507
|
+
const result = User.deserialize(obj);
|
|
1508
|
+
return Result.isOk(result);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// Usage:
|
|
1513
|
+
const result = User.deserialize('{"id":1,"email":"test@example.com"}');
|
|
1514
|
+
if (Result.isOk(result)) {
|
|
1515
|
+
const user = result.value;
|
|
1516
|
+
} else {
|
|
1517
|
+
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
|
|
1518
|
+
}
|
|
1519
|
+
```
|
|
1520
|
+
|
|
1521
|
+
Generated output:
|
|
1522
|
+
|
|
1523
|
+
```typescript
|
|
1524
|
+
import { DeserializeContext } from 'macroforge/serde';
|
|
1525
|
+
import { DeserializeError } from 'macroforge/serde';
|
|
1526
|
+
import type { DeserializeOptions } from 'macroforge/serde';
|
|
1527
|
+
import { PendingRef } from 'macroforge/serde';
|
|
1528
|
+
|
|
1529
|
+
/** @serde({ denyUnknownFields: true }) */
|
|
1530
|
+
class User {
|
|
1531
|
+
id: number;
|
|
1532
|
+
|
|
1533
|
+
email: string;
|
|
1534
|
+
|
|
1535
|
+
name: string;
|
|
1536
|
+
|
|
1537
|
+
age?: number;
|
|
1538
|
+
|
|
1539
|
+
constructor(props: {
|
|
1540
|
+
id: number;
|
|
1541
|
+
email: string;
|
|
1542
|
+
name?: string;
|
|
1543
|
+
age?: number;
|
|
1544
|
+
}) {
|
|
1545
|
+
this.id = props.id;
|
|
1546
|
+
this.email = props.email;
|
|
1547
|
+
this.name = props.name as string;
|
|
1548
|
+
this.age = props.age as number;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* Deserializes input to an instance of this class.
|
|
1553
|
+
* Automatically detects whether input is a JSON string or object.
|
|
1554
|
+
* @param input - JSON string or object to deserialize
|
|
1555
|
+
* @param opts - Optional deserialization options
|
|
1556
|
+
* @returns Result containing the deserialized instance or validation errors
|
|
1557
|
+
*/
|
|
1558
|
+
static deserialize(
|
|
1559
|
+
input: unknown,
|
|
1560
|
+
opts?: DeserializeOptions
|
|
1561
|
+
): Result<
|
|
1562
|
+
User,
|
|
1563
|
+
Array<{
|
|
1564
|
+
field: string;
|
|
1565
|
+
message: string;
|
|
1566
|
+
}>
|
|
1567
|
+
> {
|
|
1568
|
+
try {
|
|
1569
|
+
// Auto-detect: if string, parse as JSON first
|
|
1570
|
+
const data = typeof input === 'string' ? JSON.parse(input) : input;
|
|
1571
|
+
|
|
1572
|
+
const ctx = DeserializeContext.create();
|
|
1573
|
+
const resultOrRef = User.deserializeWithContext(data, ctx);
|
|
1574
|
+
if (PendingRef.is(resultOrRef)) {
|
|
1575
|
+
return Result.err([
|
|
1576
|
+
{
|
|
1577
|
+
field: '_root',
|
|
1578
|
+
message: 'User.deserialize: root cannot be a forward reference'
|
|
1579
|
+
}
|
|
1580
|
+
]);
|
|
1581
|
+
}
|
|
1582
|
+
ctx.applyPatches();
|
|
1583
|
+
if (opts?.freeze) {
|
|
1584
|
+
ctx.freezeAll();
|
|
1585
|
+
}
|
|
1586
|
+
return Result.ok(resultOrRef);
|
|
1587
|
+
} catch (e) {
|
|
1588
|
+
if (e instanceof DeserializeError) {
|
|
1589
|
+
return Result.err(e.errors);
|
|
1590
|
+
}
|
|
1591
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1592
|
+
return Result.err([
|
|
1593
|
+
{
|
|
1594
|
+
field: '_root',
|
|
1595
|
+
message
|
|
1596
|
+
}
|
|
1597
|
+
]);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/** @internal */
|
|
1602
|
+
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
|
|
1603
|
+
if (value?.__ref !== undefined) {
|
|
1604
|
+
return ctx.getOrDefer(value.__ref);
|
|
1605
|
+
}
|
|
1606
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1607
|
+
throw new DeserializeError([
|
|
1608
|
+
{
|
|
1609
|
+
field: '_root',
|
|
1610
|
+
message: 'User.deserializeWithContext: expected an object'
|
|
1611
|
+
}
|
|
1612
|
+
]);
|
|
1613
|
+
}
|
|
1614
|
+
const obj = value as Record<string, unknown>;
|
|
1615
|
+
const errors: Array<{
|
|
1616
|
+
field: string;
|
|
1617
|
+
message: string;
|
|
1618
|
+
}> = [];
|
|
1619
|
+
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
|
|
1620
|
+
for (const key of Object.keys(obj)) {
|
|
1621
|
+
if (!knownKeys.has(key)) {
|
|
1622
|
+
errors.push({
|
|
1623
|
+
field: key,
|
|
1624
|
+
message: 'unknown field'
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
if (!('id' in obj)) {
|
|
1629
|
+
errors.push({
|
|
1630
|
+
field: 'id',
|
|
1631
|
+
message: 'missing required field'
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
if (!('email' in obj)) {
|
|
1635
|
+
errors.push({
|
|
1636
|
+
field: 'email',
|
|
1637
|
+
message: 'missing required field'
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
if (errors.length > 0) {
|
|
1641
|
+
throw new DeserializeError(errors);
|
|
1642
|
+
}
|
|
1643
|
+
const instance = Object.create(User.prototype) as User;
|
|
1644
|
+
if (obj.__id !== undefined) {
|
|
1645
|
+
ctx.register(obj.__id as number, instance);
|
|
1646
|
+
}
|
|
1647
|
+
ctx.trackForFreeze(instance);
|
|
1648
|
+
{
|
|
1649
|
+
const __raw_id = obj['id'] as number;
|
|
1650
|
+
instance.id = __raw_id;
|
|
1651
|
+
}
|
|
1652
|
+
{
|
|
1653
|
+
const __raw_email = obj['email'] as string;
|
|
1654
|
+
instance.email = __raw_email;
|
|
1655
|
+
}
|
|
1656
|
+
if ('name' in obj && obj['name'] !== undefined) {
|
|
1657
|
+
const __raw_name = obj['name'] as string;
|
|
1658
|
+
instance.name = __raw_name;
|
|
1659
|
+
} else {
|
|
1660
|
+
instance.name = 'guest';
|
|
1661
|
+
}
|
|
1662
|
+
if ('age' in obj && obj['age'] !== undefined) {
|
|
1663
|
+
const __raw_age = obj['age'] as number;
|
|
1664
|
+
instance.age = __raw_age;
|
|
1665
|
+
}
|
|
1666
|
+
if (errors.length > 0) {
|
|
1667
|
+
throw new DeserializeError(errors);
|
|
1668
|
+
}
|
|
1669
|
+
return instance;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
static validateField<K extends keyof User>(
|
|
1673
|
+
field: K,
|
|
1674
|
+
value: User[K]
|
|
1675
|
+
): Array<{
|
|
1676
|
+
field: string;
|
|
1677
|
+
message: string;
|
|
1678
|
+
}> {
|
|
1679
|
+
return [];
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
static validateFields(partial: Partial<User>): Array<{
|
|
1683
|
+
field: string;
|
|
1684
|
+
message: string;
|
|
1685
|
+
}> {
|
|
1686
|
+
return [];
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
static hasShape(obj: unknown): boolean {
|
|
1690
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
const o = obj as Record<string, unknown>;
|
|
1694
|
+
return 'id' in o && 'email' in o;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
static is(obj: unknown): obj is User {
|
|
1698
|
+
if (obj instanceof User) {
|
|
1699
|
+
return true;
|
|
1700
|
+
}
|
|
1701
|
+
if (!User.hasShape(obj)) {
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
const result = User.deserialize(obj);
|
|
1705
|
+
return Result.isOk(result);
|
|
1706
|
+
}
|
|
122
1707
|
}
|
|
123
1708
|
|
|
124
1709
|
// Usage:
|
|
125
|
-
const result = User.
|
|
1710
|
+
const result = User.deserialize('{"id":1,"email":"test@example.com"}');
|
|
126
1711
|
if (Result.isOk(result)) {
|
|
127
1712
|
const user = result.value;
|
|
128
1713
|
} else {
|
|
129
|
-
console.error(result.error);
|
|
1714
|
+
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
|
|
130
1715
|
}
|
|
131
1716
|
```
|
|
132
1717
|
|