@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,26 +8,17 @@ enabling value-based equality semantics instead of reference equality.
|
|
|
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., `myTypeEquals`)
|
|
20
|
-
- `"suffix"`: Suffixes with type name (e.g., `equalsMyType`)
|
|
21
|
-
- `"generic"`: Uses TypeScript generics (e.g., `equals<T extends MyType>`)
|
|
22
|
-
- `"namespace"`: Legacy namespace wrapping
|
|
11
|
+
| Class | `classNameEquals(a, b)` + `static equals(a, b)` | Standalone function + static wrapper method |
|
|
12
|
+
| Enum | `enumNameEquals(a: EnumName, b: EnumName): boolean` | Standalone function using strict equality |
|
|
13
|
+
| Interface | `interfaceNameEquals(a: InterfaceName, b: InterfaceName): boolean` | Standalone function comparing fields |
|
|
14
|
+
| Type Alias | `typeNameEquals(a: TypeName, b: TypeName): boolean` | Standalone function with type-appropriate comparison |
|
|
23
15
|
|
|
24
16
|
## Comparison Strategy
|
|
25
17
|
|
|
26
18
|
The generated equality check:
|
|
27
19
|
|
|
28
|
-
1. **Identity check**: `
|
|
29
|
-
2. **
|
|
30
|
-
3. **Field comparison**: Compares each non-skipped field
|
|
20
|
+
1. **Identity check**: `a === b` returns true immediately
|
|
21
|
+
2. **Field comparison**: Compares each non-skipped field
|
|
31
22
|
|
|
32
23
|
## Type-Specific Comparisons
|
|
33
24
|
|
|
@@ -48,25 +39,97 @@ The `@partialEq` decorator supports:
|
|
|
48
39
|
|
|
49
40
|
## Example
|
|
50
41
|
|
|
42
|
+
```typescript before
|
|
43
|
+
/** @derive(PartialEq, Hash) */
|
|
44
|
+
class User {
|
|
45
|
+
id: number;
|
|
46
|
+
name: string;
|
|
47
|
+
|
|
48
|
+
/** @partialEq({ skip: true }) @hash({ skip: true }) */
|
|
49
|
+
cachedScore: number;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```typescript after
|
|
54
|
+
class User {
|
|
55
|
+
id: number;
|
|
56
|
+
name: string;
|
|
57
|
+
|
|
58
|
+
cachedScore: number;
|
|
59
|
+
|
|
60
|
+
static equals(a: User, b: User): boolean {
|
|
61
|
+
return userEquals(a, b);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static hashCode(value: User): number {
|
|
65
|
+
return userHashCode(value);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function userEquals(a: User, b: User): boolean {
|
|
70
|
+
if (a === b) return true;
|
|
71
|
+
return a.id === b.id && a.name === b.name;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function userHashCode(value: User): number {
|
|
75
|
+
let hash = 17;
|
|
76
|
+
hash =
|
|
77
|
+
(hash * 31 +
|
|
78
|
+
(Number.isInteger(value.id)
|
|
79
|
+
? value.id | 0
|
|
80
|
+
: value.id
|
|
81
|
+
.toString()
|
|
82
|
+
.split('')
|
|
83
|
+
.reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
|
|
84
|
+
0;
|
|
85
|
+
hash =
|
|
86
|
+
(hash * 31 +
|
|
87
|
+
(value.name ?? '').split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)) |
|
|
88
|
+
0;
|
|
89
|
+
return hash;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Generated output:
|
|
94
|
+
|
|
51
95
|
```typescript
|
|
52
|
-
@derive(PartialEq, Hash)
|
|
53
96
|
class User {
|
|
54
97
|
id: number;
|
|
55
98
|
name: string;
|
|
56
99
|
|
|
57
|
-
@partialEq(skip) // Don't compare cached values
|
|
58
|
-
@hash(skip)
|
|
59
100
|
cachedScore: number;
|
|
101
|
+
|
|
102
|
+
static equals(a: User, b: User): boolean {
|
|
103
|
+
return userEquals(a, b);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static hashCode(value: User): number {
|
|
107
|
+
return userHashCode(value);
|
|
108
|
+
}
|
|
60
109
|
}
|
|
61
110
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
111
|
+
export function userEquals(a: User, b: User): boolean {
|
|
112
|
+
if (a === b) return true;
|
|
113
|
+
return a.id === b.id && a.name === b.name;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function userHashCode(value: User): number {
|
|
117
|
+
let hash = 17;
|
|
118
|
+
hash =
|
|
119
|
+
(hash * 31 +
|
|
120
|
+
(Number.isInteger(value.id)
|
|
121
|
+
? value.id | 0
|
|
122
|
+
: value.id
|
|
123
|
+
.toString()
|
|
124
|
+
.split('')
|
|
125
|
+
.reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
|
|
126
|
+
0;
|
|
127
|
+
hash =
|
|
128
|
+
(hash * 31 +
|
|
129
|
+
(value.name ?? '').split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)) |
|
|
130
|
+
0;
|
|
131
|
+
return hash;
|
|
132
|
+
}
|
|
70
133
|
```
|
|
71
134
|
|
|
72
135
|
## Equality Contract
|
|
@@ -76,3 +139,98 @@ When implementing `PartialEq`, consider also implementing `Hash`:
|
|
|
76
139
|
- **Reflexivity**: `a.equals(a)` is always true
|
|
77
140
|
- **Symmetry**: `a.equals(b)` implies `b.equals(a)`
|
|
78
141
|
- **Hash consistency**: Equal objects must have equal hash codes
|
|
142
|
+
|
|
143
|
+
To maintain the hash contract, skip the same fields in both `PartialEq` and `Hash`:
|
|
144
|
+
|
|
145
|
+
```typescript before
|
|
146
|
+
/** @derive(PartialEq, Hash) */
|
|
147
|
+
class User {
|
|
148
|
+
id: number;
|
|
149
|
+
name: string;
|
|
150
|
+
|
|
151
|
+
/** @partialEq({ skip: true }) @hash({ skip: true }) */
|
|
152
|
+
cachedScore: number;
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```typescript after
|
|
157
|
+
class User {
|
|
158
|
+
id: number;
|
|
159
|
+
name: string;
|
|
160
|
+
|
|
161
|
+
cachedScore: number;
|
|
162
|
+
|
|
163
|
+
static equals(a: User, b: User): boolean {
|
|
164
|
+
return userEquals(a, b);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
static hashCode(value: User): number {
|
|
168
|
+
return userHashCode(value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function userEquals(a: User, b: User): boolean {
|
|
173
|
+
if (a === b) return true;
|
|
174
|
+
return a.id === b.id && a.name === b.name;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function userHashCode(value: User): number {
|
|
178
|
+
let hash = 17;
|
|
179
|
+
hash =
|
|
180
|
+
(hash * 31 +
|
|
181
|
+
(Number.isInteger(value.id)
|
|
182
|
+
? value.id | 0
|
|
183
|
+
: value.id
|
|
184
|
+
.toString()
|
|
185
|
+
.split('')
|
|
186
|
+
.reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
|
|
187
|
+
0;
|
|
188
|
+
hash =
|
|
189
|
+
(hash * 31 +
|
|
190
|
+
(value.name ?? '').split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)) |
|
|
191
|
+
0;
|
|
192
|
+
return hash;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Generated output:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
class User {
|
|
200
|
+
id: number;
|
|
201
|
+
name: string;
|
|
202
|
+
|
|
203
|
+
cachedScore: number;
|
|
204
|
+
|
|
205
|
+
static equals(a: User, b: User): boolean {
|
|
206
|
+
return userEquals(a, b);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
static hashCode(value: User): number {
|
|
210
|
+
return userHashCode(value);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function userEquals(a: User, b: User): boolean {
|
|
215
|
+
if (a === b) return true;
|
|
216
|
+
return a.id === b.id && a.name === b.name;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function userHashCode(value: User): number {
|
|
220
|
+
let hash = 17;
|
|
221
|
+
hash =
|
|
222
|
+
(hash * 31 +
|
|
223
|
+
(Number.isInteger(value.id)
|
|
224
|
+
? value.id | 0
|
|
225
|
+
: value.id
|
|
226
|
+
.toString()
|
|
227
|
+
.split('')
|
|
228
|
+
.reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
|
|
229
|
+
0;
|
|
230
|
+
hash =
|
|
231
|
+
(hash * 31 +
|
|
232
|
+
(value.name ?? '').split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)) |
|
|
233
|
+
0;
|
|
234
|
+
return hash;
|
|
235
|
+
}
|
|
236
|
+
```
|
|
@@ -8,26 +8,18 @@ between values where some pairs may be incomparable.
|
|
|
8
8
|
|
|
9
9
|
| Type | Generated Code | Description |
|
|
10
10
|
|------|----------------|-------------|
|
|
11
|
-
| Class | `compareTo(
|
|
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., `myTypePartialCompare`)
|
|
20
|
-
- `"suffix"`: Suffixes with type name (e.g., `partialCompareMyType`)
|
|
21
|
-
- `"generic"`: Uses TypeScript generics (e.g., `partialCompare<T extends MyType>`)
|
|
22
|
-
- `"namespace"`: Legacy namespace wrapping
|
|
11
|
+
| Class | `classNamePartialCompare(a, b)` + `static compareTo(a, b)` | Standalone function + static wrapper method |
|
|
12
|
+
| Enum | `enumNamePartialCompare(a: EnumName, b: EnumName): Option<number>` | Standalone function returning Option |
|
|
13
|
+
| Interface | `interfaceNamePartialCompare(a: InterfaceName, b: InterfaceName): Option<number>` | Standalone function with Option |
|
|
14
|
+
| Type Alias | `typeNamePartialCompare(a: TypeName, b: TypeName): Option<number>` | Standalone function with Option |
|
|
23
15
|
|
|
24
16
|
## Return Values
|
|
25
17
|
|
|
26
18
|
Unlike `Ord`, `PartialOrd` returns an `Option<number>` to handle incomparable values:
|
|
27
19
|
|
|
28
|
-
- **Option.some(-1)**: `
|
|
29
|
-
- **Option.some(0)**: `
|
|
30
|
-
- **Option.some(1)**: `
|
|
20
|
+
- **Option.some(-1)**: `a` is less than `b`
|
|
21
|
+
- **Option.some(0)**: `a` is equal to `b`
|
|
22
|
+
- **Option.some(1)**: `a` is greater than `b`
|
|
31
23
|
- **Option.none()**: Values are incomparable
|
|
32
24
|
|
|
33
25
|
## When to Use PartialOrd vs Ord
|
|
@@ -69,26 +61,72 @@ The `@ord` decorator supports:
|
|
|
69
61
|
|
|
70
62
|
## Example
|
|
71
63
|
|
|
64
|
+
```typescript before
|
|
65
|
+
/** @derive(PartialOrd) */
|
|
66
|
+
class Temperature {
|
|
67
|
+
value: number | null;
|
|
68
|
+
unit: string;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```typescript after
|
|
73
|
+
import { Option } from 'macroforge/utils';
|
|
74
|
+
|
|
75
|
+
class Temperature {
|
|
76
|
+
value: number | null;
|
|
77
|
+
unit: string;
|
|
78
|
+
|
|
79
|
+
static compareTo(a: Temperature, b: Temperature): Option<number> {
|
|
80
|
+
return temperaturePartialCompare(a, b);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function temperaturePartialCompare(a: Temperature, b: Temperature): Option<number> {
|
|
85
|
+
if (a === b) return Option.some(0);
|
|
86
|
+
const cmp0 = (() => {
|
|
87
|
+
if (typeof (a.value as any)?.compareTo === 'function') {
|
|
88
|
+
const optResult = (a.value as any).compareTo(b.value);
|
|
89
|
+
return Option.isNone(optResult) ? null : optResult.value;
|
|
90
|
+
}
|
|
91
|
+
return a.value === b.value ? 0 : null;
|
|
92
|
+
})();
|
|
93
|
+
if (cmp0 === null) return Option.none();
|
|
94
|
+
if (cmp0 !== 0) return Option.some(cmp0);
|
|
95
|
+
const cmp1 = a.unit.localeCompare(b.unit);
|
|
96
|
+
if (cmp1 === null) return Option.none();
|
|
97
|
+
if (cmp1 !== 0) return Option.some(cmp1);
|
|
98
|
+
return Option.some(0);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Generated output:
|
|
103
|
+
|
|
72
104
|
```typescript
|
|
73
|
-
@derive(PartialOrd)
|
|
74
105
|
class Temperature {
|
|
75
|
-
value: number | null;
|
|
106
|
+
value: number | null;
|
|
76
107
|
unit: string;
|
|
108
|
+
|
|
109
|
+
static compareTo(a: Temperature, b: Temperature): Option<number> {
|
|
110
|
+
return temperaturePartialCompare(a, b);
|
|
111
|
+
}
|
|
77
112
|
}
|
|
78
113
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
114
|
+
export function temperaturePartialCompare(a: Temperature, b: Temperature): Option<number> {
|
|
115
|
+
if (a === b) return Option.some(0);
|
|
116
|
+
const cmp0 = (() => {
|
|
117
|
+
if (typeof (a.value as any)?.compareTo === 'function') {
|
|
118
|
+
const optResult = (a.value as any).compareTo(b.value);
|
|
119
|
+
return Option.isNone(optResult) ? null : optResult.value;
|
|
120
|
+
}
|
|
121
|
+
return a.value === b.value ? 0 : null;
|
|
122
|
+
})();
|
|
123
|
+
if (cmp0 === null) return Option.none();
|
|
124
|
+
if (cmp0 !== 0) return Option.some(cmp0);
|
|
125
|
+
const cmp1 = a.unit.localeCompare(b.unit);
|
|
126
|
+
if (cmp1 === null) return Option.none();
|
|
127
|
+
if (cmp1 !== 0) return Option.some(cmp1);
|
|
128
|
+
return Option.some(0);
|
|
129
|
+
}
|
|
92
130
|
```
|
|
93
131
|
|
|
94
132
|
## Required Import
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
## Example
|
|
2
|
+
|
|
3
|
+
```typescript before
|
|
4
|
+
/** @derive(Serialize) */
|
|
5
|
+
class User {
|
|
6
|
+
id: number;
|
|
7
|
+
|
|
8
|
+
/** @serde({ rename: "userName" }) */
|
|
9
|
+
name: string;
|
|
10
|
+
|
|
11
|
+
/** @serde({ skipSerializing: true }) */
|
|
12
|
+
password: string;
|
|
13
|
+
|
|
14
|
+
/** @serde({ flatten: true }) */
|
|
15
|
+
metadata: UserMetadata;
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```typescript after
|
|
20
|
+
import { SerializeContext } from 'macroforge/serde';
|
|
21
|
+
|
|
22
|
+
class User {
|
|
23
|
+
id: number;
|
|
24
|
+
|
|
25
|
+
name: string;
|
|
26
|
+
|
|
27
|
+
password: string;
|
|
28
|
+
|
|
29
|
+
metadata: UserMetadata;
|
|
30
|
+
/** Serializes a value to a JSON string.
|
|
31
|
+
@param value - The value to serialize
|
|
32
|
+
@returns JSON string representation with cycle detection metadata */
|
|
33
|
+
|
|
34
|
+
static serialize(value: User): string {
|
|
35
|
+
return userSerialize(value);
|
|
36
|
+
}
|
|
37
|
+
/** @internal Serializes with an existing context for nested/cyclic object graphs.
|
|
38
|
+
@param value - The value to serialize
|
|
39
|
+
@param ctx - The serialization context */
|
|
40
|
+
|
|
41
|
+
static serializeWithContext(value: User, ctx: SerializeContext): Record<string, unknown> {
|
|
42
|
+
return userSerializeWithContext(value, ctx);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Serializes a value to a JSON string.
|
|
47
|
+
@param value - The value to serialize
|
|
48
|
+
@returns JSON string representation with cycle detection metadata */ export function userSerialize(
|
|
49
|
+
value: User
|
|
50
|
+
): string {
|
|
51
|
+
const ctx = SerializeContext.create();
|
|
52
|
+
return JSON.stringify(userSerializeWithContext(value, ctx));
|
|
53
|
+
} /** @internal Serializes with an existing context for nested/cyclic object graphs.
|
|
54
|
+
@param value - The value to serialize
|
|
55
|
+
@param ctx - The serialization context */
|
|
56
|
+
export function userSerializeWithContext(
|
|
57
|
+
value: User,
|
|
58
|
+
ctx: SerializeContext
|
|
59
|
+
): Record<string, unknown> {
|
|
60
|
+
const existingId = ctx.getId(value);
|
|
61
|
+
if (existingId !== undefined) {
|
|
62
|
+
return { __ref: existingId };
|
|
63
|
+
}
|
|
64
|
+
const __id = ctx.register(value);
|
|
65
|
+
const result: Record<string, unknown> = { __type: 'User', __id };
|
|
66
|
+
result['id'] = value.id;
|
|
67
|
+
result['userName'] = value.name;
|
|
68
|
+
{
|
|
69
|
+
const __flattened = userMetadataSerializeWithContext(value.metadata, ctx);
|
|
70
|
+
const { __type: _, __id: __, ...rest } = __flattened as any;
|
|
71
|
+
Object.assign(result, rest);
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Generated output:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { SerializeContext } from 'macroforge/serde';
|
|
81
|
+
|
|
82
|
+
class User {
|
|
83
|
+
id: number;
|
|
84
|
+
|
|
85
|
+
name: string;
|
|
86
|
+
|
|
87
|
+
password: string;
|
|
88
|
+
|
|
89
|
+
metadata: UserMetadata;
|
|
90
|
+
/** Serializes a value to a JSON string.
|
|
91
|
+
@param value - The value to serialize
|
|
92
|
+
@returns JSON string representation with cycle detection metadata */
|
|
93
|
+
|
|
94
|
+
static serialize(value: User): string {
|
|
95
|
+
return userSerialize(value);
|
|
96
|
+
}
|
|
97
|
+
/** @internal Serializes with an existing context for nested/cyclic object graphs.
|
|
98
|
+
@param value - The value to serialize
|
|
99
|
+
@param ctx - The serialization context */
|
|
100
|
+
|
|
101
|
+
static serializeWithContext(value: User, ctx: SerializeContext): Record<string, unknown> {
|
|
102
|
+
return userSerializeWithContext(value, ctx);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Serializes a value to a JSON string.
|
|
107
|
+
@param value - The value to serialize
|
|
108
|
+
@returns JSON string representation with cycle detection metadata */ export function userSerialize(
|
|
109
|
+
value: User
|
|
110
|
+
): string {
|
|
111
|
+
const ctx = SerializeContext.create();
|
|
112
|
+
return JSON.stringify(userSerializeWithContext(value, ctx));
|
|
113
|
+
} /** @internal Serializes with an existing context for nested/cyclic object graphs.
|
|
114
|
+
@param value - The value to serialize
|
|
115
|
+
@param ctx - The serialization context */
|
|
116
|
+
export function userSerializeWithContext(
|
|
117
|
+
value: User,
|
|
118
|
+
ctx: SerializeContext
|
|
119
|
+
): Record<string, unknown> {
|
|
120
|
+
const existingId = ctx.getId(value);
|
|
121
|
+
if (existingId !== undefined) {
|
|
122
|
+
return { __ref: existingId };
|
|
123
|
+
}
|
|
124
|
+
const __id = ctx.register(value);
|
|
125
|
+
const result: Record<string, unknown> = { __type: 'User', __id };
|
|
126
|
+
result['id'] = value.id;
|
|
127
|
+
result['userName'] = value.name;
|
|
128
|
+
{
|
|
129
|
+
const __flattened = userMetadataSerializeWithContext(value.metadata, ctx);
|
|
130
|
+
const { __type: _, __id: __, ...rest } = __flattened as any;
|
|
131
|
+
Object.assign(result, rest);
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Required Import
|
|
138
|
+
|
|
139
|
+
The generated code automatically imports `SerializeContext` from `macroforge/serde`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Serialize
|
|
2
|
+
|
|
3
|
+
The `Serialize` macro generates JSON serialization methods with **cycle detection**
|
|
4
|
+
and object identity tracking. This enables serialization of complex object graphs
|
|
5
|
+
including circular references.
|
|
6
|
+
|
|
7
|
+
## Generated Methods
|
|
8
|
+
|
|
9
|
+
| Type | Generated Code | Description |
|
|
10
|
+
|------|----------------|-------------|
|
|
11
|
+
| Class | `classNameSerialize(value)` + `static serialize(value)` | Standalone function + static wrapper method |
|
|
12
|
+
| Enum | `enumNameSerialize(value)`, `enumNameSerializeWithContext` | Standalone functions |
|
|
13
|
+
| Interface | `interfaceNameSerialize(value)`, etc. | Standalone functions |
|
|
14
|
+
| Type Alias | `typeNameSerialize(value)`, etc. | Standalone functions |
|
|
15
|
+
|
|
16
|
+
## Cycle Detection Protocol
|
|
17
|
+
|
|
18
|
+
The generated code handles circular references using `__id` and `__ref` markers:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"__type": "User",
|
|
23
|
+
"__id": 1,
|
|
24
|
+
"name": "Alice",
|
|
25
|
+
"friend": { "__ref": 2 } // Reference to object with __id: 2
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
When an object is serialized:
|
|
30
|
+
1. Check if it's already been serialized (has an `__id`)
|
|
31
|
+
2. If so, return `{ "__ref": existingId }` instead
|
|
32
|
+
3. Otherwise, register the object and serialize its fields
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
## Type-Specific Serialization
|
|
2
|
+
|
|
3
|
+
| Type | Serialization Strategy |
|
|
4
|
+
|------|------------------------|
|
|
5
|
+
| Primitives | Direct value |
|
|
6
|
+
| `Date` | `toISOString()` |
|
|
7
|
+
| Arrays | For primitive-like element types, pass through; for `Date`/`Date | null`, map to ISO strings; otherwise map and call `SerializeWithContext(ctx)` when available |
|
|
8
|
+
| `Map<K,V>` | For primitive-like values, `Object.fromEntries(map.entries())`; for `Date`/`Date | null`, convert to ISO strings; otherwise call `SerializeWithContext(ctx)` per value when available |
|
|
9
|
+
| `Set<T>` | Convert to array; element handling matches `Array<T>` |
|
|
10
|
+
| Nullable | Include `null` explicitly; for primitive-like and `Date` unions the generator avoids runtime `SerializeWithContext` checks |
|
|
11
|
+
| Objects | Call `SerializeWithContext(ctx)` if available (to support user-defined implementations) |
|
|
12
|
+
|
|
13
|
+
Note: the generator specializes some code paths based on the declared TypeScript type to
|
|
14
|
+
avoid runtime feature detection on primitives and literal unions.
|
|
15
|
+
|
|
16
|
+
## Field-Level Options
|
|
17
|
+
|
|
18
|
+
The `@serde` decorator supports:
|
|
19
|
+
|
|
20
|
+
- `skip` / `skipSerializing` - Exclude field from serialization
|
|
21
|
+
- `rename = "jsonKey"` - Use different JSON property name
|
|
22
|
+
- `flatten` - Merge nested object's fields into parent
|