tyzen-macro 0.2.4

Procedural macros for the tyzen crate.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
<div align="center">
  <img height="150" src="assets/logo/logo.svg" alt="Tyzen Logo" />
  <h1>Tyzen</h1>
  <p><b>Type-safe Rust ↔ TypeScript generation</b></p>
  <a href="https://crates.io/crates/tyzen"><img src="https://img.shields.io/crates/v/tyzen.svg?style=flat-square" alt="crates.io version" /></a>
  <a href="https://crates.io/crates/tyzen"><img src="https://img.shields.io/crates/d/tyzen.svg?style=flat-square" alt="downloads" /></a>
  <a href="https://docs.rs/tyzen"><img src="https://img.shields.io/docsrs/tyzen?style=flat-square" alt="docs.rs" /></a>
  <a href="https://crates.io/crates/tyzen"><img src="https://img.shields.io/crates/l/tyzen.svg?style=flat-square" alt="license" /></a>
</div>

Tyzen is a high-performance developer tool that bridges the gap between **Rust** and **TypeScript**. Its primary goal is to provide a seamless, type-safe development experience (DX) by automating the synchronization of data structures and API definitions.

While Tyzen was built with **Tauri** in mind, its core is a generic, lightning-fast type generator that can be used in any Rust/TS project.

## Quick Links

- [Quick Start]#quick-start
- [Tauri Integration]#tauri-integration
- [Generated Output]#example-generated-output-bindingsts
- [Namespace Guide]#namespace-pattern
- [Error Guide]#error-handling-pattern
- [Zod Guide]#zod-schema-generation
- [Typed Events]#typed-events
- [Feature Guide]#-feature-guide
- [Benchmark Results]#benchmark-results
- [Roadmap]#-roadmap--status

## Why Tyzen?

- **Performance**: Generates complex TypeScript bindings in milliseconds.
- **Developer Experience (DX)**: Zero-config command registration and strongly-typed events.
- **Transparency**: Keeps your code clear and explicit.
- **Tauri Integration**: First-class support for Tauri commands and events with automatic registration.

## Benchmark Results

Latest committed benchmark artifacts are in [`tyzen/bench/results`](./tyzen/bench/results).

| Benchmark               |    Median    |     P95      | Notes                                                                            |
| :---------------------- | :----------: | :----------: | :------------------------------------------------------------------------------- |
| `e2e-generate`          |  `0.132 ms`  |  `0.233 ms`  | Real `tyzen::generate(...)` flow, small realistic fixture (`2824` bytes output). |
| `e2e-heavy-generate`    |  `5.993 ms`  |  `7.193 ms`  | Real `tyzen::generate(...)` flow, heavy fixture (`542292` bytes output).         |
| `cold-process-generate` | `293.804 ms` | `961.796 ms` | Full cold process startup + generate (includes process bootstrap variance).      |
| `codegen-large`         |  `3.404 ms`  |  `3.939 ms`  | Synthetic renderer throughput (`1000` types, `24` fields/type).                  |

Reproduce locally:

```bash
cargo bench -p tyzen --bench codegen
cargo bench -p tyzen --bench e2e
cargo bench -p tyzen --bench e2e_heavy
./tyzen/bench/scripts/run_cold_generate.sh 6 1
```

For full benchmark methodology and output formats, see [`tyzen/bench/README.md`](./tyzen/bench/README.md).

---

## Comparison: Tyzen vs tauri-specta vs ts-rs

Tyzen already covers the core job people usually reach for `ts-rs` for: deriving TypeScript from Rust structs/enums with Serde-aware naming, optional fields, flattening, tagged enums, generics, and deterministic output.

The bigger difference is scope. `ts-rs` is a mature type-export crate. `tauri-specta` is part of the Specta stack, where `specta` provides type introspection/export, `rspc` provides end-to-end typed APIs, and `tauri-specta` provides typed Tauri commands. Tyzen is narrower than the full Specta ecosystem, but deeper on the Tauri DX it owns: generated command wrappers, events, namespaces, binary hydration, result/error handling, constants, and a predictable single-file SDK-style output.
Tyzen's killer DX feature for Tauri teams is that `.invoke_handler(tyzen_tauri::handler!())` auto-collects all macro-registered handlers, while Specta-based setups typically require explicit manual handler lists, which is easier to drift and gives weaker command-surface autocomplete ergonomics.

| Capability                    | Tyzen                                                        | tauri-specta                                                         | ts-rs                                                      |
| :---------------------------- | :----------------------------------------------------------- | :------------------------------------------------------------------- | :--------------------------------------------------------- |
| Primary focus                 | Rust -> TS type generation + Tauri command/event DX          | Typed Tauri commands/events on top of Specta                         | Rust -> TS type generation                                 |
| Tauri command wrappers        | ✅ Built-in via `tyzen-tauri`                                | ✅ Built-in                                                          | ❌ Not primary scope                                       |
| Event typing for Tauri        | ✅ Built-in event helpers                                    | ✅ Supported                                                         | ❌ Not primary scope                                       |
| Namespace-oriented SDK output | ✅ First-class (`module_ns!`, rename strategy)               | ⚠️ Depends on your Specta export style                               | ⚠️ Manual conventions                                      |
| Serde-oriented type mapping   | ✅ Core goal                                                 | ✅ Via Specta + serde integrations                                   | ✅ Core goal                                               |
| Export granularity            | Single generated bindings file by default                    | Exporter-driven                                                      | Per-type export and dependency export                      |
| Advanced per-field overrides  | Partial (`rename`, `skip`, `flatten`, `optional`, `binary`)  | Specta-driven                                                        | Mature (`type`, `as`, `inline`, `skip`, optional variants) |
| Generic edge cases            | Common generics                                              | Strong type graph model                                              | Mature escape hatches (`concrete`, `bound`)                |
| External crate coverage       | Growing (`uuid`, `chrono`, `anyhow` features)                | Broad feature ecosystem                                              | Broad Rust type coverage                                   |
| Ecosystem coupling            | Low to medium (Tyzen crates)                                 | Medium to high (Specta/rspc/tauri-specta alignment)                  | Low                                                        |
| Good default when...          | You want typed Tauri IPC + predictable generated API surface | You already use Specta/rspc or want a shared type graph across tools | You only need mature standalone TS type exports            |

### Decision Guide

- Choose **Tyzen** when you want type export, Tauri command wiring, event helpers, and frontend ergonomics in one explicit generator flow.
- Choose **tauri-specta** when you already use Specta/rspc, want to share one type graph across that stack, or prefer Specta's exporter model.
- Choose **ts-rs** when your use case is pure model syncing and you need its mature escape hatches for custom type overrides, inlining, concrete generics, or per-type export layout.

Reference links:

- `tauri-specta`: https://github.com/specta-rs/tauri-specta
- `specta`: https://github.com/specta-rs/specta
- `ts-rs`: https://github.com/Aleph-Alpha/ts-rs

---

<a id="quick-start"></a>

## Quick Start

### 1. Add Dependencies

For core type generation:

```bash
cargo add tyzen
cargo add serde --features derive
```

For **Tauri** integration (Optional):

```bash
cargo add tyzen-tauri
```

### 2. Define Your Types (Rust)

Tyzen uses simple attributes to mark structs, enums, and functions.

```rust
use serde::{Serialize, Deserialize};

// 1. Convert any Rust struct/enum to TS
#[derive(tyzen::Type, Serialize, Deserialize)]
pub struct User {
    pub id: u32,
    pub name: String,
}

// 2. Define a typed Event
#[derive(tyzen::Type, tyzen::Event, Serialize)]
pub struct WelcomeEvent {
    pub message: String,
}
```

### 3. Setup the Generator

Run generation before frontend type-check/build so `bindings.ts` always matches Rust.

- Core (non-Tauri):

```rust
fn main() {
    tyzen::generate("../src/bindings.ts").expect("failed to generate bindings");
}
```

- Tauri (recommended default):

```rust
fn main() {
    #[cfg(debug_assertions)]
    tyzen_tauri::generate("../src/bindings.ts").expect("failed to generate bindings");

    tauri::Builder::default()
        .invoke_handler(tyzen_tauri::handler!())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

- Tauri with config (for example `Option<T> -> field?: T`):

```rust
fn main() {
    #[cfg(debug_assertions)]
    tyzen_tauri::generate_with_config(
        "../src/bindings.ts",
        tyzen::GeneratorConfig {
            option_fields_as_optional: true,
            ..Default::default()
        },
    )
    .expect("failed to generate bindings");

    tauri::Builder::default()
        .invoke_handler(tyzen_tauri::handler!())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

- Non-Tauri workflow (`package.json`):

```rust
// src/bin/gen_bindings.rs
fn main() {
    tyzen::generate("../frontend/src/bindings.ts").expect("failed to generate bindings");
}
```

```json
{
  "scripts": {
    "gen:bindings": "cargo run --bin gen_bindings",
    "build": "pnpm gen:bindings && tsc -b && vite build"
  }
}
```

If backend is in another workspace package:

```bash
cargo run -p backend --bin gen_bindings
```

- Tyzen rewrites the target file only when generated content actually changes.

---

<a id="tauri-integration"></a>

## Tauri Integration

For Tauri projects, `tyzen-tauri` provides a wrapper that automates command registration and event handling.

### 1. Define Tauri Commands

No stacked macros needed! The `#[tyzen_tauri::command]` macro automatically expands and registers your commands with Tauri under the hood.

```rust
#[tyzen_tauri::command] // Marks for TS generation & auto-registers with Tauri
pub fn create_user(name: String) -> Result<User, String> {
    Ok(User { id: 1, name })
}
```

### 2. Setup Generator & Handler

```rust
fn main() {
    // 1. Generate TS bindings with Tauri support
    #[cfg(debug_assertions)]
    tyzen_tauri::generate("../src/bindings.ts").expect("failed to generate bindings");

    // 2. Setup Tauri with auto-registration
    tauri::Builder::default()
        .invoke_handler(tyzen_tauri::handler!()) // Auto-registers all #[tyzen_tauri::command]
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

---

## Frontend Usage (TypeScript/React)

Tyzen creates a clean, intuitive API for your frontend.

```tsx
import { useEffect, useState } from 'react';
import { commands, events } from './bindings';

function App() {
  const [status, setStatus] = useState('Ready');

  useEffect(() => {
    // Listen once when component mounts
    const unlisten = events.welcome.listen(payload => {
      setStatus(`Message: ${payload.message}`);
    });

    return () => {
      unlisten.then(f => f());
    };
  }, []);

  const handleCreateUser = async () => {
    // Command calls can be made anywhere (button click, form submit, effect, etc.)
    const res = await commands.createUser('rzust');
    if (res.status === 'ok') console.log('Success:', res.data);
  };

  return (
    <>
      <button onClick={handleCreateUser}>Create user</button>
      <h1>{status}</h1>
    </>
  );
}
```

### Example Generated Output (`bindings.ts`)

Tyzen output is plain TypeScript. This is what your frontend imports directly:

```ts
// auto-generated by tyzen, do not edit
export type User = { id: number; name: string };

export type Result<T, E = string> =
  | { status: 'ok'; data: T }
  | { status: 'error'; error: E };

export const commands = {
  createUser: (name: string) => __invoke<Result<User>>('create_user', { name }),
};

export const events = {
  onWelcome: (cb: (payload: { message: string }) => void) =>
    __listen('welcome', cb),
};
```

What this gives you:

- Rust snake_case command names become camelCase frontend functions.
- Return values are strongly typed (`Result<User>` in this example).
- Event payloads are typed at subscription sites.

---

## Feature Guide

### Standard Type Conversion

Use `#[derive(tyzen::Type)]` on any Rust type. It supports primitives, `Vec`, `Option`, `HashMap`, and even complex **Generics**.

### Tauri Commands

The `#[tyzen_tauri::command]` macro is the only attribute you need to declare a type-safe Tauri command. It automatically handles code generation and expands `#[tauri::command]` so there is zero boilerplate or stacked-macro footguns.

<a id="namespace-pattern"></a>

## Namespace Pattern

Use namespaces when you want an SDK-like frontend API (`Task.getAll()`) instead of one flat command list.

How namespace resolution works:

- `tyzen::module_ns!("Task")` sets a default namespace for that module tree.
- `#[tyzen(ns = "...")]` on a specific type/command/event overrides module default.
- Output includes both:
  - global type definitions (`TaskItem`, `User`, ...)
  - namespaced action objects (`Task`, `Auth`, ...)

Command naming inside a namespace:

- Default strategy is `Prefix`.
- With namespace `Task`, `task_get_all` becomes `Task.getAll`.
- `Postfix` strategy is also supported, so `get_all_task` becomes `Task.getAll`.
- `#[tyzen(rename = "...")]` on a command wins over auto strip logic.

```rust
tyzen::module_ns!("Task");

#[derive(tyzen::Type)]
pub struct TaskItem {
    pub id: u64,
    pub title: String,
}

#[tyzen::command]
pub fn task_get_all() -> Vec<TaskItem> {
    vec![]
}
```

Generated frontend shape:

```ts
import { Task } from './bindings';

const res = await Task.getAll();
```

If your team prefers postfix names (`get_all_task` style), set naming strategy:

```rust
tyzen_tauri::generate_with_config(
    "../src/bindings.ts",
    tyzen::GeneratorConfig {
        naming_strategy: tyzen::NamingStrategy::Postfix,
        ..Default::default()
    },
)?;
```

Per-command explicit rename (works for both core and tauri command attrs):

```rust
#[tyzen::command(ns = "Task", rename = "getAll")]
pub fn task_get_all() -> Vec<TaskItem> {
    vec![]
}
```

<a id="error-handling-pattern"></a>

## Error Handling Pattern

Keep backend errors typed, then map them to user-facing messages in frontend.

```ts
import { parseError } from './bindings/helpers';
import { ProjectErrorMeta } from './bindings';

const res = await commands.projectCreate(payload);
if (res.status === 'error') {
  const uiError = parseError(res.error, ProjectErrorMeta);
  console.error(uiError.code, uiError.message);
}
```

<a id="zod-schema-generation"></a>

## Zod Schema Generation

What is already supported:

- Opt-in schema generation with `#[tyzen(schema)]`.
- Struct schema generation (`z.object(...)`) for common field types.
- Unit enums to `z.enum([...])`.
- Basic validation sync (`min/max/regex`, numeric bounds).
- Generated inferred schema aliases (`z.infer<typeof ...>`).

What will be added next:

- Tagged/untagged payload enum schemas (discriminated union style support).
- Better explicit fallback visibility when generation must use `z.any()`.
- Broader external type mapping and deeper nested schema coverage.

Rust example:

```rust
#[derive(tyzen::Type)]
#[tyzen(schema)]
pub struct CreateProjectDto {
    #[validate(length(min = 2, max = 60))]
    pub title: String,
    pub priority: Priority,
    pub target_date: Option<String>,
}

#[derive(tyzen::Type)]
pub enum Priority {
    Low,
    Medium,
    High,
}
```

Generated output example (`bindings.ts`):

```ts
import { z } from 'zod';

export type CreateProjectDto = {
  title: string;
  priority: Priority;
  target_date: string | null;
};

export const prioritySchema = z.enum(['Low', 'Medium', 'High']);
export type PrioritySchema = z.infer<typeof prioritySchema>;

export const createProjectDtoSchema = z.object({
  title: z.string().min(2).max(60),
  priority: z.enum(['Low', 'Medium', 'High']),
  target_date: z.union([z.string(), z.date()]).nullable().optional(),
});
export type CreateProjectDtoSchema = z.infer<typeof createProjectDtoSchema>;
```

<a id="typed-events"></a>

## Typed Events

When you derive `tyzen::Event`, Tyzen adds a helper `.emit(&handle)` method to your struct:

```rust
let event = WelcomeEvent { message: "Hi!".into() };
event.emit(&handle).ok(); // Correctly types the payload for the frontend
```

---

## Roadmap & Status

| Feature               | Importance     | Notes                                                                                                             |
| :-------------------- | :------------- | :---------------------------------------------------------------------------------------------------------------- |
| **Full Serde Parity** | Implemented    | `flatten`, `alias`, `default`, and `rename_all` support. Requires inter-type metadata.                            |
| **Binary Data**       | Implemented    | Automatically maps `Vec<u8>` or fields marked with `#[tyzen(binary)]` to `Uint8Array` with transparent hydration. |
| **Result & Error**    | Implemented    | Deep support for custom Rust error types and enum variant metadata blocks in frontend.                            |
| **Constant Export**   | Implemented    | Sync `pub const` logic values from Rust to TS.                                                                    |
| **Namespaces**        | Implemented    | Organize types and commands into logical Models (SDK style).                                                      |
| **Zod Support**       | Partial        | Generate Zod schemas alongside types for runtime frontend validation.                                             |
| **Mock Client**       | Will implement | Generate mock JS/TS clients for testing/UI prototyping without the backend.                                       |
| **Doc Propagation**   |                | Transform Rust doc comments (`///`) into TSDoc (`/** ... */`).                                                    |

---

## Packages

- `tyzen`: The core engine for type conversion.
- `tyzen-macro`: Procedural macros for `Type` and `Event`.
- `tyzen-tauri`: Specialized integration for Tauri (commands, event emitters, and TS glue code).

## Contributing

You can contribute even if this is your first OSS project:

- Open an issue describing bug/feature before large changes.
- Fork, create a branch, and open a PR with a focused diff.
- Add or update tests for behavior changes in `tyzen/tests` or `tyzen-tauri/tests`.
- Include before/after snippets for generated TypeScript when relevant.

Small first contributions that help a lot:

- Add coverage for edge-case Serde mappings.
- Improve docs/examples around command/event patterns.
- Add mappings for common external Rust types.

## License

Distributed under the MIT / Apache-2.0 License.