macroforge 0.1.33 → 0.1.34

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/js/serde/index.ts CHANGED
@@ -1,21 +1,104 @@
1
1
  /**
2
- * Serde runtime helpers for macroforge Serialize/Deserialize macros.
3
- * These are used by the generated code to handle cycles, forward references,
4
- * and polymorphic deserialization.
2
+ * # Macroforge Serde Module
3
+ *
4
+ * This module provides runtime helpers for the `Serialize` and `Deserialize` macros.
5
+ * It handles complex serialization scenarios including:
6
+ *
7
+ * - **Cycle Detection**: Objects are assigned unique `__id` values during serialization.
8
+ * When the same object is encountered again, a `{ "__ref": id }` marker is emitted
9
+ * instead of re-serializing the object.
10
+ *
11
+ * - **Forward References**: During deserialization, references to objects that haven't
12
+ * been created yet are tracked as `PendingRef` markers. After all objects are
13
+ * instantiated, `applyPatches()` resolves these references.
14
+ *
15
+ * - **Validation Errors**: The `DeserializeError` class collects structured field-level
16
+ * errors that can be displayed to users.
17
+ *
18
+ * ## Serialization Flow
19
+ *
20
+ * ```typescript
21
+ * // Generated code creates a context
22
+ * const ctx = SerializeContext.create();
23
+ *
24
+ * // Each object gets registered with a unique ID
25
+ * const id = ctx.register(obj);
26
+ *
27
+ * // Before serializing, check if already seen
28
+ * const existingId = ctx.getId(obj);
29
+ * if (existingId !== undefined) {
30
+ * return { __ref: existingId }; // Return reference marker
31
+ * }
32
+ * ```
33
+ *
34
+ * ## Deserialization Flow
35
+ *
36
+ * ```typescript
37
+ * // Generated code creates a context
38
+ * const ctx = DeserializeContext.create();
39
+ *
40
+ * // Register objects as they're created
41
+ * ctx.register(id, instance);
42
+ *
43
+ * // References are resolved immediately if available, or deferred
44
+ * const value = ctx.getOrDefer(refId);
45
+ *
46
+ * // After all objects are created, resolve pending references
47
+ * ctx.applyPatches();
48
+ * ```
49
+ *
50
+ * @module macroforge/serde
5
51
  */
6
52
 
7
53
  // ============================================================================
8
54
  // Serialization Context
9
55
  // ============================================================================
10
56
 
57
+ /**
58
+ * Context for tracking objects during serialization.
59
+ *
60
+ * The context assigns unique IDs to objects as they're serialized,
61
+ * enabling cycle detection. When an object is encountered that has
62
+ * already been serialized, a `{ "__ref": id }` marker is emitted instead.
63
+ */
11
64
  export interface SerializeContext {
12
- /** Get the ID for an already-registered object, or undefined if not seen */
65
+ /**
66
+ * Gets the ID for an already-registered object.
67
+ * @param obj - The object to look up
68
+ * @returns The object's ID, or `undefined` if not yet registered
69
+ */
13
70
  getId(obj: object): number | undefined;
14
- /** Register an object and return its assigned ID */
71
+
72
+ /**
73
+ * Registers an object and assigns it a unique ID.
74
+ * @param obj - The object to register
75
+ * @returns The newly assigned ID
76
+ */
15
77
  register(obj: object): number;
16
78
  }
17
79
 
80
+ /**
81
+ * Factory functions for creating serialization contexts.
82
+ */
18
83
  export namespace SerializeContext {
84
+ /**
85
+ * Creates a new serialization context.
86
+ *
87
+ * The context uses a `WeakMap` to track objects, so objects can be
88
+ * garbage collected if no other references exist.
89
+ *
90
+ * @returns A new `SerializeContext` instance
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * const ctx = SerializeContext.create();
95
+ * const id = ctx.register(myObject);
96
+ * // Later...
97
+ * if (ctx.getId(myObject) !== undefined) {
98
+ * // Object was already serialized, emit reference
99
+ * }
100
+ * ```
101
+ */
19
102
  export function create(): SerializeContext {
20
103
  const ids = new WeakMap<object, number>();
21
104
  let nextId = 0;
@@ -34,25 +117,107 @@ export namespace SerializeContext {
34
117
  // Deserialization Context
35
118
  // ============================================================================
36
119
 
120
+ /**
121
+ * Context for tracking objects and resolving references during deserialization.
122
+ *
123
+ * The context maintains a registry of objects by their `__id` values and
124
+ * collects "patches" for forward references that need to be resolved after
125
+ * all objects are created.
126
+ *
127
+ * ## Forward Reference Resolution
128
+ *
129
+ * When deserializing circular or forward references:
130
+ *
131
+ * 1. Object A references Object B (which hasn't been created yet)
132
+ * 2. A `PendingRef` marker is stored temporarily
133
+ * 3. Object B is created and registered with its ID
134
+ * 4. `applyPatches()` resolves A's reference to point to B
135
+ */
37
136
  export interface DeserializeContext {
38
- /** Register an object with a known ID */
137
+ /**
138
+ * Registers an object with a known ID.
139
+ * @param id - The object's ID from the `__id` field
140
+ * @param instance - The deserialized object instance
141
+ */
39
142
  register(id: number, instance: any): void;
40
- /** Get an object by ID, or return a PendingRef if not yet available */
143
+
144
+ /**
145
+ * Gets an object by ID, or returns a `PendingRef` if not yet available.
146
+ * @param refId - The ID from the `__ref` field
147
+ * @returns The object if already registered, or a `PendingRef` marker
148
+ */
41
149
  getOrDefer(refId: number): any;
42
- /** Defer a patch using a setter callback (called when ref is resolved) */
43
- deferPatch(refId: number, setter: (val: any) => void): void;
44
- /** Track an object for optional freezing */
150
+
151
+ /**
152
+ * Assigns a value to a property, deferring if it's a `PendingRef`.
153
+ * If the value is a `PendingRef`, the assignment is recorded as a patch
154
+ * to be applied later.
155
+ * @param target - The object to assign to
156
+ * @param prop - The property name or index
157
+ * @param value - The value to assign (may be a `PendingRef`)
158
+ */
159
+ assignOrDefer(target: any, prop: string | number, value: any): void;
160
+
161
+ /**
162
+ * Manually adds a patch for later resolution.
163
+ * @param target - The object containing the reference
164
+ * @param prop - The property name or index
165
+ * @param refId - The ID of the referenced object
166
+ */
167
+ addPatch(target: any, prop: string | number, refId: number): void;
168
+
169
+ /**
170
+ * Tracks an object for optional freezing after deserialization.
171
+ * @param obj - The object to track
172
+ */
45
173
  trackForFreeze(obj: object): void;
46
- /** Apply all deferred patches (call after deserialization is complete) */
174
+
175
+ /**
176
+ * Applies all deferred patches, resolving forward references.
177
+ * Call this after all objects have been created.
178
+ * @throws Error if any referenced ID is not in the registry
179
+ */
47
180
  applyPatches(): void;
48
- /** Freeze all tracked objects (call after applyPatches if immutability is desired) */
181
+
182
+ /**
183
+ * Freezes all tracked objects for immutability.
184
+ * Call this after `applyPatches()` if immutable objects are desired.
185
+ */
49
186
  freezeAll(): void;
50
187
  }
51
188
 
189
+ /**
190
+ * Factory functions for creating deserialization contexts.
191
+ */
52
192
  export namespace DeserializeContext {
193
+ /**
194
+ * Creates a new deserialization context.
195
+ *
196
+ * The context maintains:
197
+ * - A registry mapping IDs to deserialized objects
198
+ * - A list of patches for forward references
199
+ * - A list of objects to freeze (if immutability is enabled)
200
+ *
201
+ * @returns A new `DeserializeContext` instance
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * const ctx = DeserializeContext.create();
206
+ *
207
+ * // Register objects as they're created
208
+ * ctx.register(1, user);
209
+ * ctx.register(2, friend);
210
+ *
211
+ * // Resolve forward references
212
+ * ctx.applyPatches();
213
+ *
214
+ * // Optionally freeze for immutability
215
+ * ctx.freezeAll();
216
+ * ```
217
+ */
53
218
  export function create(): DeserializeContext {
54
219
  const registry = new Map<number, any>();
55
- const patches: Array<{ refId: number; setter: (val: any) => void }> = [];
220
+ const patches: Array<{ target: any; prop: string | number; refId: number }> = [];
56
221
  const toFreeze: object[] = [];
57
222
 
58
223
  return {
@@ -67,8 +232,17 @@ export namespace DeserializeContext {
67
232
  return PendingRef.create(refId);
68
233
  },
69
234
 
70
- deferPatch: (refId, setter) => {
71
- patches.push({ refId, setter });
235
+ assignOrDefer: (target, prop, value) => {
236
+ if (PendingRef.is(value)) {
237
+ target[prop] = null;
238
+ patches.push({ target, prop, refId: value.id });
239
+ } else {
240
+ target[prop] = value;
241
+ }
242
+ },
243
+
244
+ addPatch: (target, prop, refId) => {
245
+ patches.push({ target, prop, refId });
72
246
  },
73
247
 
74
248
  trackForFreeze: (obj) => {
@@ -76,11 +250,11 @@ export namespace DeserializeContext {
76
250
  },
77
251
 
78
252
  applyPatches: () => {
79
- for (const { refId, setter } of patches) {
253
+ for (const { target, prop, refId } of patches) {
80
254
  if (!registry.has(refId)) {
81
255
  throw new Error(`Unresolved reference: __ref ${refId}`);
82
256
  }
83
- setter(registry.get(refId));
257
+ target[prop] = registry.get(refId);
84
258
  }
85
259
  },
86
260
 
@@ -97,17 +271,38 @@ export namespace DeserializeContext {
97
271
  // Pending Reference Marker
98
272
  // ============================================================================
99
273
 
100
- /** Marker interface for forward references that need patching */
274
+ /**
275
+ * Marker interface for forward references that need patching.
276
+ *
277
+ * When deserializing a `{ "__ref": id }` marker for an object that hasn't
278
+ * been created yet, a `PendingRef` is stored temporarily. After all objects
279
+ * are created, `DeserializeContext.applyPatches()` resolves these markers.
280
+ */
101
281
  export interface PendingRef {
282
+ /** Discriminant field to identify pending references */
102
283
  readonly __pendingRef: true;
284
+ /** The ID of the referenced object */
103
285
  readonly id: number;
104
286
  }
105
287
 
288
+ /**
289
+ * Factory and type guard functions for `PendingRef`.
290
+ */
106
291
  export namespace PendingRef {
292
+ /**
293
+ * Creates a new pending reference marker.
294
+ * @param id - The ID of the referenced object
295
+ * @returns A `PendingRef` marker
296
+ */
107
297
  export function create(id: number): PendingRef {
108
298
  return { __pendingRef: true, id };
109
299
  }
110
300
 
301
+ /**
302
+ * Type guard to check if a value is a `PendingRef`.
303
+ * @param value - The value to check
304
+ * @returns `true` if the value is a `PendingRef`
305
+ */
111
306
  export function is(value: any): value is PendingRef {
112
307
  return (
113
308
  value !== null &&
@@ -122,8 +317,15 @@ export namespace PendingRef {
122
317
  // Options for fromStringifiedJSON
123
318
  // ============================================================================
124
319
 
320
+ /**
321
+ * Options for configuring deserialization behavior.
322
+ */
125
323
  export interface DeserializeOptions {
126
- /** If true, freeze all deserialized objects after patching */
324
+ /**
325
+ * If `true`, all deserialized objects are frozen after patching.
326
+ * This provides immutability guarantees but prevents modification.
327
+ * @default false
328
+ */
127
329
  freeze?: boolean;
128
330
  }
129
331
 
@@ -131,16 +333,57 @@ export interface DeserializeOptions {
131
333
  // Structured Error for Deserialization
132
334
  // ============================================================================
133
335
 
134
- /** Structured field error for validation failures */
336
+ /**
337
+ * Structured error for a single field validation failure.
338
+ *
339
+ * Used by the `Deserialize` macro to collect validation errors
340
+ * in a format suitable for display to users.
341
+ */
135
342
  export interface FieldError {
343
+ /**
344
+ * The field path that failed validation.
345
+ * For nested fields, uses dot notation (e.g., `"address.street"`).
346
+ */
136
347
  field: string;
348
+
349
+ /**
350
+ * Human-readable error message describing the validation failure.
351
+ * @example "must be a valid email"
352
+ * @example "must have at least 3 characters"
353
+ */
137
354
  message: string;
138
355
  }
139
356
 
140
- /** Error class that carries structured field errors */
357
+ /**
358
+ * Error class that carries structured field validation errors.
359
+ *
360
+ * Thrown by deserialization methods when validation fails. Contains
361
+ * an array of `FieldError` objects that can be displayed to users
362
+ * or used for form validation feedback.
363
+ *
364
+ * @example
365
+ * ```typescript
366
+ * try {
367
+ * const user = User.fromStringifiedJSON(json);
368
+ * } catch (e) {
369
+ * if (e instanceof DeserializeError) {
370
+ * for (const { field, message } of e.errors) {
371
+ * console.error(`${field}: ${message}`);
372
+ * }
373
+ * }
374
+ * }
375
+ * ```
376
+ */
141
377
  export class DeserializeError extends Error {
378
+ /**
379
+ * Array of field-level validation errors.
380
+ */
142
381
  public readonly errors: FieldError[];
143
382
 
383
+ /**
384
+ * Creates a new deserialization error.
385
+ * @param errors - Array of field validation errors
386
+ */
144
387
  constructor(errors: FieldError[]) {
145
388
  const message = errors.map((e) => `${e.field}: ${e.message}`).join("; ");
146
389
  super(message);
@@ -1,33 +1,258 @@
1
1
  /**
2
- * Trait interfaces for macroforge derive macros.
3
- * All members are readonly and use function-style signatures (for namespace compatibility).
2
+ * # Macroforge Traits Module
3
+ *
4
+ * This module defines TypeScript interfaces that correspond to Rust-like traits.
5
+ * These interfaces describe the shape of the methods generated by Macroforge's
6
+ * derive macros for interfaces, enums, and type aliases.
7
+ *
8
+ * For **classes**, the generated methods are instance methods (e.g., `user.clone()`).
9
+ * For **interfaces/enums/type aliases**, the generated methods are namespace
10
+ * functions that take the value as the first parameter (e.g., `User.clone(user)`).
11
+ *
12
+ * ## Usage
13
+ *
14
+ * These traits are primarily used for type checking generated code:
15
+ *
16
+ * ```typescript
17
+ * import type { Clone, Debug } from "macroforge/traits";
18
+ *
19
+ * // Type-check that a namespace implements Clone
20
+ * const UserNs: Clone<User> = User;
21
+ * ```
22
+ *
23
+ * @module macroforge/traits
24
+ */
25
+ /**
26
+ * Trait for types that can be deep-copied.
27
+ *
28
+ * Analogous to Rust's `Clone` trait. The generated `clone` method creates
29
+ * an independent copy of the value.
30
+ *
31
+ * @template T - The type being cloned
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * // For interfaces/type aliases, use as namespace:
36
+ * const cloned = UserNs.clone(original);
37
+ *
38
+ * // For classes, methods are on the instance:
39
+ * const cloned = original.clone();
40
+ * ```
4
41
  */
5
42
  export interface Clone<T> {
43
+ /**
44
+ * Creates a deep copy of the value.
45
+ * @param self - The value to clone
46
+ * @returns A new independent copy of the value
47
+ */
6
48
  readonly clone: (self: T) => T;
7
49
  }
50
+ /**
51
+ * Trait for types with a human-readable string representation.
52
+ *
53
+ * Analogous to Rust's `Debug` trait. The generated `toString` method
54
+ * produces a string like `"TypeName { field1: value1, field2: value2 }"`.
55
+ *
56
+ * @template T - The type being formatted
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * console.log(User.toString(user));
61
+ * // Output: "User { id: 1, name: Alice }"
62
+ * ```
63
+ */
8
64
  export interface Debug<T> {
65
+ /**
66
+ * Returns a debug string representation of the value.
67
+ * @param self - The value to format
68
+ * @returns A human-readable string for debugging
69
+ */
9
70
  readonly toString: (self: T) => string;
10
71
  }
72
+ /**
73
+ * Trait for types with a default value.
74
+ *
75
+ * Analogous to Rust's `Default` trait. The generated `defaultValue` method
76
+ * creates an instance with all fields set to their default values.
77
+ *
78
+ * @template T - The type being constructed
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * const user = User.defaultValue();
83
+ * // Creates: { id: 0, name: "", active: false }
84
+ * ```
85
+ */
11
86
  export interface Default<T> {
87
+ /**
88
+ * Creates a new instance with default values for all fields.
89
+ * @returns A new instance with default field values
90
+ */
12
91
  readonly defaultValue: () => T;
13
92
  }
93
+ /**
94
+ * Trait for types that support equality comparison.
95
+ *
96
+ * Analogous to Rust's `PartialEq` trait. The generated `equals` method
97
+ * performs structural equality comparison on all non-skipped fields.
98
+ *
99
+ * @template T - The type being compared
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * if (User.equals(user1, user2)) {
104
+ * console.log("Users are equal");
105
+ * }
106
+ * ```
107
+ */
14
108
  export interface PartialEq<T> {
109
+ /**
110
+ * Compares two values for equality.
111
+ * @param self - The first value
112
+ * @param other - The second value (accepts unknown for flexibility)
113
+ * @returns `true` if the values are equal, `false` otherwise
114
+ */
15
115
  readonly equals: (self: T, other: unknown) => boolean;
16
116
  }
117
+ /**
118
+ * Trait for types that can produce a hash code.
119
+ *
120
+ * Analogous to Rust's `Hash` trait. The generated `hashCode` method
121
+ * computes a 32-bit integer hash suitable for use in hash-based collections.
122
+ *
123
+ * **Hash Contract**: Objects that are equal (via `PartialEq`) must produce
124
+ * the same hash code.
125
+ *
126
+ * @template T - The type being hashed
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const hash = User.hashCode(user);
131
+ * // Use in Map: map.set(hash, user);
132
+ * ```
133
+ */
17
134
  export interface Hash<T> {
135
+ /**
136
+ * Computes a hash code for the value.
137
+ * @param self - The value to hash
138
+ * @returns A 32-bit integer hash code
139
+ */
18
140
  readonly hashCode: (self: T) => number;
19
141
  }
142
+ /**
143
+ * Trait for types with partial ordering.
144
+ *
145
+ * Analogous to Rust's `PartialOrd` trait. The generated `compareTo` method
146
+ * returns a number for comparable values, or `null` for incomparable values.
147
+ *
148
+ * @template T - The type being compared
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * const cmp = User.compareTo(user1, user2);
153
+ * if (cmp !== null) {
154
+ * if (cmp < 0) console.log("user1 < user2");
155
+ * else if (cmp > 0) console.log("user1 > user2");
156
+ * else console.log("user1 == user2");
157
+ * } else {
158
+ * console.log("Incomparable");
159
+ * }
160
+ * ```
161
+ */
20
162
  export interface PartialOrd<T> {
163
+ /**
164
+ * Compares two values for ordering.
165
+ * @param self - The first value
166
+ * @param other - The second value
167
+ * @returns `-1` if self < other, `0` if equal, `1` if self > other, or `null` if incomparable
168
+ */
21
169
  readonly compareTo: (self: T, other: unknown) => number | null;
22
170
  }
171
+ /**
172
+ * Trait for types with total ordering.
173
+ *
174
+ * Analogous to Rust's `Ord` trait. The generated `compareTo` method
175
+ * always returns a number (never null) - all values are comparable.
176
+ *
177
+ * @template T - The type being compared
178
+ *
179
+ * @example
180
+ * ```typescript
181
+ * // Sort an array using Ord
182
+ * users.sort((a, b) => User.compareTo(a, b));
183
+ * ```
184
+ */
23
185
  export interface Ord<T> {
186
+ /**
187
+ * Compares two values for ordering (total order).
188
+ * @param self - The first value
189
+ * @param other - The second value
190
+ * @returns `-1` if self < other, `0` if equal, `1` if self > other
191
+ */
24
192
  readonly compareTo: (self: T, other: T) => number;
25
193
  }
194
+ /**
195
+ * Trait for types that can be serialized to JSON.
196
+ *
197
+ * Analogous to Rust's serde `Serialize` trait. The generated methods
198
+ * convert objects to JSON with cycle detection via `__id`/`__ref` markers.
199
+ *
200
+ * @template T - The type being serialized
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * const json = User.toStringifiedJSON(user);
205
+ * // => '{"__type":"User","__id":1,"name":"Alice"}'
206
+ *
207
+ * const obj = User.toObject(user);
208
+ * // => { __type: "User", __id: 1, name: "Alice" }
209
+ * ```
210
+ */
26
211
  export interface Serialize<T> {
212
+ /**
213
+ * Serializes the value to a JSON string.
214
+ * @param self - The value to serialize
215
+ * @returns JSON string with `__type` and `__id` markers
216
+ */
27
217
  readonly toStringifiedJSON: (self: T) => string;
218
+ /**
219
+ * Converts the value to a plain JavaScript object.
220
+ * @param self - The value to convert
221
+ * @returns Plain object suitable for JSON.stringify
222
+ */
28
223
  readonly toObject: (self: T) => Record<string, unknown>;
29
224
  }
225
+ /**
226
+ * Trait for types that can be deserialized from JSON.
227
+ *
228
+ * Analogous to Rust's serde `Deserialize` trait. The generated methods
229
+ * parse JSON, resolve `__ref` markers, and validate field values.
230
+ *
231
+ * @template T - The type being deserialized
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * import { Result } from "macroforge/utils";
236
+ *
237
+ * const result = User.fromStringifiedJSON(json);
238
+ * if (Result.isOk(result)) {
239
+ * const user = result.value;
240
+ * } else {
241
+ * console.error(result.error); // Validation errors
242
+ * }
243
+ * ```
244
+ */
30
245
  export interface Deserialize<T> {
246
+ /**
247
+ * Deserializes a value from a JSON string.
248
+ * @param json - JSON string to parse
249
+ * @returns The deserialized value (may throw on invalid input)
250
+ */
31
251
  readonly fromStringifiedJSON: (json: string) => T;
252
+ /**
253
+ * Deserializes a value from a plain JavaScript object.
254
+ * @param obj - Object to deserialize from
255
+ * @returns The deserialized value (may throw on invalid input)
256
+ */
32
257
  readonly fromObject: (obj: unknown) => T;
33
258
  }