@macroforge/mcp-server
Version:
MCP server for Macroforge documentation and code analysis
218 lines (152 loc) • 7.27 kB
Markdown
# The Derive System
The derive system is inspired by Rust's derive macros. It allows you to automatically implement
common patterns by annotating your classes with ``.
## Syntax Reference
Macroforge uses JSDoc comments for all macro annotations. This ensures compatibility with standard
TypeScript tooling.
### The Statement
The `` decorator triggers macro expansion on a class or interface:
Source
TypeScript
```
/** @derive(Debug) */
class MyClass {
value: string;
}
```
Syntax rules:
- Must be inside a JSDoc comment (`/** */`)
- Must appear immediately before the class/interface declaration
- Multiple macros can be comma-separated: `(A, B, C)`
- Multiple `` statements can be stacked
Source
TypeScript
```
/** @derive(Debug, Clone) */
class User {
name: string;
email: string;
}
```
### The import macro Statement
To use macros from external packages, you must declare them with `import macro`:
TypeScript
```
/** import macro { MacroName } from "package-name"; */
```
Syntax rules:
- Must be inside a JSDoc comment (`/** */`)
- Can appear anywhere in the file (typically at the top)
- Multiple macros can be imported: `import macro { A, B } from "pkg";`
- Multiple import statements can be used for different packages
TypeScript
```
/** import macro { JSON, Validate } from "@my/macros"; */
/** import macro { Builder } from "@other/macros"; */
/** @derive(JSON, Validate, Builder) */
class User {
name: string;
email: string;
}
```
Built-in macros
Built-in macros (Debug, Clone, Default, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize) do
not require an import statement.
### Field Attributes
Macros can define field-level attributes to customize behavior per field:
Before (Your Code)
```
/** @derive(Debug, Serialize) */
class User {
/** @debug({ rename: "userId" }) */
/** @serde({ rename: "user_id" }) */
id: number;
name: string;
/** @debug({ skip: true }) */
/** @serde({ skip: true }) */
password: string;
metadata: Record<string, unknown>;
}
```
After (Generated)
```
import { SerializeContext as __mf_SerializeContext } from "@macroforge/core/serde";
class User {
id: number;
name: string;
password: string;
metadata: Record<string, unknown>;
static toString(value: User): string {
return userToString(value);
}
/** Serializes a value to a JSON string. @param value - The value to serialize @param keepMetadata - If true, preserves __type and __id fields in the output @returns JSON string representation */
static serialize(value: User, keepMetadata?: boolean): string {
return userSerialize(value, keepMetadata);
}
/** @internal Serializes with an existing context for nested/cyclic object graphs. @param value - The value to serialize @param ctx - The serialization context */
static serializeWithContext(value: User, ctx: __mf_SerializeContext): Record<string, unknown> {
return userSerializeWithContext(value, ctx);
}}
export function userToString(value: User): string {
const parts: string[] = [];
parts.push("userId: " + value.id);
parts.push("name: " + value.name);
parts.push("metadata: " + value.metadata);
return "User { " + parts.join(", ") + " }";
}
/** Serializes a value to a JSON string. @param value - The value to serialize @param keepMetadata - If true, preserves __type and __id fields in the output @returns JSON string representation */
export function userSerialize(value: User, keepMetadata?: boolean): string {
const ctx = __mf_SerializeContext.create();
const __raw = userSerializeWithContext(value, ctx);
if (keepMetadata) return JSON.stringify(__raw);
return JSON.stringify(__raw, (key, val) => key === "__type" || key === "__id" ? undefined : val);
}
/** @internal Serializes with an existing context for nested/cyclic object graphs. @param value - The value to serialize @param ctx - The serialization context */
export function userSerializeWithContext(value: User, ctx: __mf_SerializeContext): Record<string, unknown> {
const existingId = ctx.getId(value);
if (existingId !== undefined) {
return { __ref: existingId };
}
const __id = ctx.register(value);
const result: Record<string, unknown> = {
"__type": "User",
__id,
};result.user_id = value.id;result.name = value.name;result.metadata = value.metadata;
return result;
}
```
Syntax rules:
- Must be inside a JSDoc comment immediately before the field
- Options use object literal syntax: `({ key: value })`
- Boolean options: `({ skip: true })`
- String options: `({ rename: "newName" })`
- Multiple attributes can be on separate lines or combined
Common field attributes by macro:
| Macro | Attribute | Options |
| --------------------- | ------------- | -------------------------------------- |
| Debug | `` | `skip`, `rename` |
| Clone | `` | `skip`, `clone_with` |
| Serialize/Deserialize | `` | `skip`, `rename`, `flatten`, `default` |
| Hash | `` | `skip` |
| PartialEq/Ord | ``, `` | `skip` |
## How It Works
1. **Declaration**: You write `(MacroName)` before a class
2. **Discovery**: Macroforge finds all derive decorators in your code
3. **Expansion**: Each named macro receives the class AST and generates code
4. **Injection**: Generated methods/properties are added to the class
## What Can Be Derived
The derive system works on:
- **Classes**: The primary target for derive macros
- **Interfaces**: Macros generate companion namespace functions
- **Enums**: Macros generate namespace functions for enum values
- **Type aliases**: Both object types and union types are supported
## Built-in vs Custom Macros
Macroforge comes with built-in macros that work out of the box. You can also create custom macros in
Rust and use them via the `import macro` statement.
| Type | Import Required | Examples |
| -------- | --------------- | ------------------------------------------------------------------------------- |
| Built-in | No | Debug, Clone, Default, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize |
| Custom | Yes | Any macro from an external package |
## Next Steps
- [Explore built-in macros](../../docs/builtin-macros)
- [Create custom macros](../../docs/custom-macros)