typespec-rust-emitter 0.1.0 → 0.2.0

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.
@@ -0,0 +1,11 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(just *)",
5
+ "Bash(npm run *)",
6
+ "Bash(npm test)",
7
+ "Bash(cargo check)"
8
+ ]
9
+ },
10
+ "$version": 3
11
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.2.0] - 2026-03-27
9
+
10
+ ### Changed
11
+
12
+ - **Breaking**: Server trait now uses associated type instead of generic parameter
13
+ - `pub trait Server<Claims>` → `pub trait Server` with `type Claims: Send + Sync + 'static`
14
+ - Users must now define their own `Claims` type instead of using the generated one
15
+ - **Breaking**: `create_router` signature changed
16
+ - Now accepts middleware function: `create_router<S, M>(service: S, middleware: M)`
17
+ - Middleware type: `FnOnce(Router<S>) -> Router<S>`
18
+ - Protected routes wrapped with: `middleware(protected)`
19
+ - Router now applies `.with_state(service)` once at the end instead of per-route
20
+ - Removed generated `Claims` struct from `types.rs`
21
+
22
+ ### Added
23
+
24
+ - Demo server implementation in `example/output-rust/src/main.rs`
25
+ - Full server trait documentation in README.md
26
+
27
+ ### Benefits
28
+
29
+ - Cleaner API - Claims type defined by user, not generated
30
+ - Flexible middleware - any function matching the trait bound
31
+ - No duplicate `.with_state(service.clone())` calls
32
+ - Better separation of concerns - auth logic owned by user
33
+
34
+ ## [0.1.0] - 2026-03-27
35
+
36
+ ### Added
37
+
38
+ - Initial release
39
+ - TypeSpec to Rust code generation for:
40
+ - Models → `pub struct` with serde derives
41
+ - Enums → `pub enum` with serde rename
42
+ - Unions → `Option<T>` for nullable types
43
+ - Scalars → `pub type` or `pub struct` with pattern validation
44
+ - Error models → `thiserror::Error` derive
45
+ - Custom derives via `@rustDerive` and `@rustDerives` decorators
46
+ - Server trait generation for axum HTTP operations
47
+ - Support for `@useAuth` decorator to mark protected routes
48
+ - Type mappings for common TypeSpec types and formats
package/DEV.md ADDED
@@ -0,0 +1,81 @@
1
+ # TypeSpec Rust Emitter - LLM Context
2
+
3
+ ## Purpose
4
+
5
+ TypeSpec emitter that generates idiomatic Rust code (structs, enums, server traits) from TypeSpec specifications.
6
+
7
+ ## Key Paths
8
+
9
+ | Path | Purpose |
10
+ | ------------------------------------ | ------------------------------------------------------- |
11
+ | `src/emitter.ts` | Main emitter logic (1464 lines) - TypeSpec→Rust codegen |
12
+ | `src/lib.tsp` | Decorator declarations (`@rustDerive`, `@rustDerives`) |
13
+ | `test/hello.test.ts` | Unit tests using `emit()` helper |
14
+ | `example/lib/learning/` | Demo TypeSpec models & operations |
15
+ | `example/output-rust/src/generated/` | Generated Rust output |
16
+ | `oas3-gen/crates/oas3-gen/` | Reference template for codegen patterns |
17
+
18
+ ## Commands (Run After Every Change)
19
+
20
+ ```bash
21
+ just build # npm run build - Compile TypeScript
22
+ just test # npm test - Run unit tests
23
+ just compile # tsp compile example → generates Rust code
24
+ just check-rust # cargo check - Verify Rust compiles
25
+ ```
26
+
27
+ ## Type Mappings
28
+
29
+ | TypeSpec | Rust |
30
+ | ---------------------- | ----------------------- |
31
+ | `string` | `String` |
32
+ | `int32/64` | `i32/i64` |
33
+ | `T \| null` | `Option<T>` |
34
+ | `T[]` | `Vec<T>` |
35
+ | `Record<T>` | `HashMap<String, T>` |
36
+ | `@format("uuid")` | `uuid::Uuid` |
37
+ | `@format("date-time")` | `chrono::DateTime<Utc>` |
38
+
39
+ ## Architecture
40
+
41
+ 1. `navigateProgram()` walks TypeSpec AST
42
+ 2. Collects models, enums, operations
43
+ 3. Processes decorators
44
+ 4. Generates Rust code via string templates
45
+ 5. Emits to `example/output-rust/src/generated/`
46
+
47
+ ## Server Trait Generation
48
+
49
+ - Generates `Server<Claims>` trait with async methods per operation
50
+ - Handler functions: `pub async fn {op}_handler<S, Claims>(...)`
51
+ - All handlers use `<S, Claims>` generics (even public routes)
52
+ - Protected routes (`@useAuth`) receive `claims: Claims` parameter
53
+ - Router splits public/protected routes, merges at end
54
+
55
+ ## Test Pattern
56
+
57
+ ```typescript
58
+ import { emit } from "./test-host.js";
59
+ const results = await emit(`model User { name: string; }`);
60
+ const output = results["types.rs"];
61
+ strictEqual(output.includes("pub struct User"), true);
62
+ ```
63
+
64
+ ## Recent Fix (2026-03-27)
65
+
66
+ All handler functions now use `<S, Claims>` generics uniformly. Public routes include `Claims: Send + Sync + Clone + 'static` bound even without using Claims, because Server trait requires it.
67
+
68
+ ## Rules
69
+
70
+ 1. Always run full cycle: `build → test → compile → check-rust`
71
+ 2. TypeScript strict mode enabled - no implicit any
72
+ 3. Match existing code patterns in `src/emitter.ts`
73
+ 4. Add tests for new features
74
+ 5. Generated Rust must compile
75
+
76
+ ## Todo
77
+
78
+ - [ ] More server trait test coverage
79
+ - [ ] Integration tests with Rust compilation
80
+ - [ ] Expand scalar format mappings
81
+ - [ ] Full working Rust server example
package/QWEN.md ADDED
@@ -0,0 +1,216 @@
1
+ # QWEN.md - Project Context
2
+
3
+ ## Project Overview
4
+
5
+ **TypeSpec Rust Emitter** - A TypeScript-based code generator that converts TypeSpec specifications into idiomatic Rust code.
6
+
7
+ - **Type**: TypeScript/Node.js project (TypeSpec emitter)
8
+ - **Output**: Generates Rust structs, enums, server traits with serde, thiserror, axum support
9
+ - **Main Entry**: `src/emitter.ts` (1450 lines) - core codegen logic
10
+ - **Package**: `typespec-rust-emitter` v0.2.0
11
+
12
+ ## Directory Structure
13
+
14
+ ```
15
+ typespec-emitter/
16
+ ├── src/
17
+ │ ├── emitter.ts # MAIN: TypeSpec→Rust code generation
18
+ │ ├── index.ts # Exports ($onEmit, $rustDerive, $rustDerives)
19
+ │ ├── lib.ts # TypeSpec library definition
20
+ │ ├── lib.tsp # Decorator declarations
21
+ │ └── testing/ # Test utilities
22
+ ├── test/
23
+ │ ├── hello.test.ts # Unit tests (11 tests)
24
+ │ └── test-host.ts # Test infrastructure (emit() helper)
25
+ ├── example/
26
+ │ ├── main.tsp # Demo TypeSpec entry
27
+ │ ├── lib/learning/ # Demo models & operations
28
+ │ ├── tspconfig.yaml # Emitter configuration
29
+ │ └── output-rust/ # Generated Rust crate
30
+ ├── oas3-gen/ # Reference template repo
31
+ ├── dist/ # Compiled TypeScript output
32
+ ├── package.json # Dependencies & scripts
33
+ ├── tsconfig.json # TypeScript config (strict mode)
34
+ └── justfile # Build automation
35
+ ```
36
+
37
+ ## Build & Run Commands
38
+
39
+ ### Primary Workflow (Run After Every Change)
40
+ ```bash
41
+ just build # Compile TypeScript (npm run build)
42
+ just test # Run unit tests (npm test)
43
+ just compile # Generate Rust code from example
44
+ just check-rust # Verify Rust compiles (cargo check)
45
+ ```
46
+
47
+ ### Individual Commands
48
+ ```bash
49
+ # Development
50
+ npm run build # tsc compile
51
+ npm run watch # Watch mode
52
+ npm run lint # ESLint check
53
+ npm run lint:fix # Auto-fix lint
54
+ npm run format # Prettier format
55
+ npm run format:check
56
+
57
+ # Full pipeline
58
+ just install # npm install (root + example)
59
+ just publish # build + npm publish
60
+ ```
61
+
62
+ ## Type Mappings (TypeSpec → Rust)
63
+
64
+ | TypeSpec | Rust |
65
+ |----------|------|
66
+ | `string` | `String` |
67
+ | `int8/16/32/64` | `i8/i16/i32/i64` |
68
+ | `uint8/16/32/64` | `u8/u16/u32/u64` |
69
+ | `float32/64` | `f32/f64` |
70
+ | `boolean` | `bool` |
71
+ | `bytes` | `Vec<u8>` |
72
+ | `T[]` | `Vec<T>` |
73
+ | `Record<T>` | `HashMap<String, T>` |
74
+ | `T \| null` | `Option<T>` |
75
+
76
+ ### Format Mappings
77
+ | `@format()` | Rust Type |
78
+ |-------------|-----------|
79
+ | `"uuid"` | `uuid::Uuid` |
80
+ | `"date"` | `chrono::NaiveDate` |
81
+ | `"time"` | `chrono::NaiveTime` |
82
+ | `"date-time"` | `chrono::DateTime<Utc>` |
83
+
84
+ ## Decorators
85
+
86
+ | Decorator | Effect |
87
+ |-----------|--------|
88
+ | `@error` | Adds `thiserror::Error` derive + `#[error("{code}: {message}")]` |
89
+ | `@pattern("regex")` | Generates `TryFrom<String>` with regex validation |
90
+ | `@rustDerive("...")` | Adds custom derive (e.g., `sqlx::FromRow`) |
91
+ | `@rustDerives("...", "...")` | Adds multiple custom derives |
92
+ | `@doc("...")` | Generates `///` doc comments |
93
+ | `@useAuth(BearerAuth)` | Marks operation as protected (adds `Claims` param) |
94
+ | `@maxLength(n)` | Documentation only |
95
+
96
+ ## Output Files
97
+
98
+ ### types.rs
99
+ - All models → `pub struct` with serde derives
100
+ - Enums → `pub enum` with serde rename
101
+ - Unions → `Option<T>` for nullable, or sum types
102
+ - Scalars → `pub type` or `pub struct` (if pattern validation)
103
+
104
+ ### server.rs
105
+ - `Server` trait with `type Claims` associated type
106
+ - Request structs per operation
107
+ - Response enums with status code variants
108
+ - `create_router<S, M>(service: S, middleware: M)` function with axum routes
109
+
110
+ ## Key Implementation Details
111
+
112
+ ### emitter.ts Architecture
113
+ 1. `navigateProgram()` - Walks TypeSpec AST
114
+ 2. Collects: models, enums, unions, scalars, operations
115
+ 3. Processes decorators (`@error`, `@pattern`, `@rustDerive`)
116
+ 4. Generates Rust via string templates
117
+ 5. Emits files to configured output directory
118
+
119
+ ### Server Trait Generation
120
+ - Generates `Server` trait with `type Claims: Send + Sync + 'static` associated type
121
+ - Handler functions: `pub async fn {op}_handler<S>(...)` with `<S>` generics only
122
+ - Protected routes (`@useAuth`) receive `claims: Self::Claims` via `Extension<Self::Claims>`
123
+ - Public routes don't require claims
124
+ - `create_router<S, M>(service: S, middleware: M)` accepts middleware function
125
+ - Middleware wraps protected routes: `middleware(protected)`
126
+ - Router merges public/protected, applies `.with_state(service)` once at end
127
+
128
+ ### Test Pattern
129
+ ```typescript
130
+ import { emit } from "./test-host.js";
131
+ const results = await emit(`model User { name: string; }`);
132
+ const output = results["types.rs"];
133
+ strictEqual(output.includes("pub struct User"), true);
134
+ ```
135
+
136
+ ## Dependencies
137
+
138
+ ### Peer (Required)
139
+ - `@typespec/compiler` >=1.0.0
140
+ - `@typespec/emitter-framework` ^0.17.0
141
+
142
+ ### Dev
143
+ - `typescript` ^5.3.3
144
+ - `eslint` ^9.15.0
145
+ - `prettier` ^3.3.3
146
+
147
+ ### Generated Rust (example/output-rust/Cargo.toml)
148
+ ```toml
149
+ serde = { version = "1.0", features = ["derive"] }
150
+ thiserror = "2.0"
151
+ uuid = { version = "1.23", features = ["serde"] }
152
+ chrono = { version = "0.4", features = ["serde"] }
153
+ regex = "1.12"
154
+ sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "derive"] }
155
+ axum = "0.8"
156
+ eyre = "0.6"
157
+ async-trait = "0.1"
158
+ tokio = { version = "1.50", features = ["full"] }
159
+ ```
160
+
161
+ ## Configuration Files
162
+
163
+ ### tspconfig.yaml
164
+ ```yaml
165
+ emit:
166
+ - "@typespec/openapi3"
167
+ - "typespec-rust-emitter"
168
+ options:
169
+ "typespec-rust-emitter":
170
+ emitter-output-dir: "{output-dir}/../output-rust/src/generated"
171
+ ```
172
+
173
+ ### tsconfig.json
174
+ - Target: ES2022
175
+ - Module: NodeNext
176
+ - Strict: true
177
+ - Root/Out: `.` → `dist`
178
+
179
+ ## Development Conventions
180
+
181
+ 1. **TypeScript**: Strict mode, ES2022, NodeNext modules
182
+ 2. **Testing**: Node.js built-in test runner (`node:test`)
183
+ 3. **Formatting**: Prettier (yaml config)
184
+ 4. **Linting**: ESLint + typescript-eslint
185
+ 5. **Code Style**: Match existing patterns in `src/emitter.ts`
186
+
187
+ ## Rules
188
+
189
+ 1. Always run full cycle after changes: `build → test → compile → check-rust`
190
+ 2. Add tests for new features in `test/hello.test.ts`
191
+ 3. Generated Rust must compile (verify with `just check-rust`)
192
+ 4. Reference `oas3-gen/` for advanced codegen patterns
193
+ 5. No implicit any (strict TypeScript)
194
+
195
+ ## Recent Changes
196
+
197
+ ### 2026-03-27: v0.2.0 - Server Trait Refactoring
198
+
199
+ **Changes**:
200
+ - `pub trait Server<Claims>` → `pub trait Server` with `type Claims` associated type
201
+ - Removed generated `Claims` struct from `types.rs` (users define their own)
202
+ - `create_router<S, M>(service: S, middleware: M)` now accepts middleware function
203
+ - Middleware wraps protected routes: `middleware(protected)`
204
+ - Single `.with_state(service)` call at end of router creation
205
+
206
+ **Benefits**:
207
+ - Cleaner API - Claims type defined by user, not generated
208
+ - Flexible middleware - any function `FnOnce(Router<S>) -> Router<S>`
209
+ - No duplicate `.with_state(service.clone())` calls
210
+
211
+ ## TODO
212
+
213
+ - [ ] More server trait test coverage
214
+ - [ ] Integration tests with Rust compilation
215
+ - [ ] Expand scalar format mappings (more chrono types)
216
+ - [ ] Support additional HTTP decorators
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- Authored by [opencode](https://opencode.ai)
1
+ Authored by [opencode](https://opencode.ai) & Qwen
2
2
 
3
3
  ---
4
4
 
@@ -19,6 +19,7 @@ A TypeSpec emitter that generates idiomatic Rust types and structs from TypeSpec
19
19
  - **Error Models**: Generates `thiserror::Error` derive with `#[error(...)]` attributes
20
20
  - **Pattern Validation**: Supports `@pattern` decorators with `TryFrom<String>` validation
21
21
  - **Custom Derives**: Add arbitrary Rust derive macros via `@rustDerive` decorator
22
+ - **Server Trait**: Generates axum server trait and router from HTTP operations
22
23
 
23
24
  ## Installation
24
25
 
@@ -36,8 +37,6 @@ npm install @typespec/compiler @typespec/emitter-framework
36
37
 
37
38
  ## Usage
38
39
 
39
- ## Usage
40
-
41
40
  ### Basic Model
42
41
 
43
42
  ```typespec
@@ -179,6 +178,136 @@ pub struct GroupStatistics {
179
178
  }
180
179
  ```
181
180
 
181
+ ## Server Trait Generation
182
+
183
+ The emitter can generate a complete axum server trait and router from TypeSpec HTTP operations.
184
+
185
+ ### Example TypeSpec
186
+
187
+ ```typespec
188
+ @route("/groups")
189
+ namespace Groups {
190
+ @get
191
+ @route("")
192
+ op list(): Group[];
193
+
194
+ @post
195
+ @route("")
196
+ @useAuth(BearerAuth)
197
+ op create(@body body: CreateGroupBody): Group;
198
+
199
+ @get
200
+ @route("/{id}")
201
+ op getById(@path id: int64): Group;
202
+ }
203
+ ```
204
+
205
+ ### Generated Server Trait
206
+
207
+ ```rust
208
+ #[async_trait]
209
+ pub trait Server: Send + Sync {
210
+ type Claims: Send + Sync + 'static;
211
+
212
+ async fn groups_list(&self, request: GroupsListRequest) -> Result<GroupsListResponse>;
213
+ async fn groups_create(&self, claims: Self::Claims, request: GroupsCreateRequest) -> Result<GroupsCreateResponse>;
214
+ async fn groups_get_by_id(&self, request: GroupsGetByIdRequest) -> Result<GroupsGetByIdResponse>;
215
+ }
216
+ ```
217
+
218
+ ### Implementing the Server
219
+
220
+ ```rust
221
+ #[derive(Clone)]
222
+ pub struct AppState {
223
+ pub db: Arc<SqlitePool>,
224
+ }
225
+
226
+ pub struct MyServer {
227
+ state: AppState,
228
+ }
229
+
230
+ #[async_trait::async_trait]
231
+ impl Server for MyServer {
232
+ type Claims = Claims; // Your custom claims type
233
+
234
+ async fn groups_list(
235
+ &self,
236
+ _request: GroupsListRequest,
237
+ ) -> eyre::Result<GroupsListResponse> {
238
+ let groups = sqlx::query_as::<_, Group>("SELECT * FROM groups")
239
+ .fetch_all(&self.state.db)
240
+ .await?;
241
+ Ok(GroupsListResponse::Ok(Json(groups)))
242
+ }
243
+
244
+ async fn groups_create(
245
+ &self,
246
+ claims: Self::Claims,
247
+ request: GroupsCreateRequest,
248
+ ) -> eyre::Result<GroupsCreateResponse> {
249
+ // claims.sub contains the authenticated user ID
250
+ let group = sqlx::query_as::<_, Group>(
251
+ "INSERT INTO groups (name, owner_id) VALUES ($1, $2) RETURNING *"
252
+ )
253
+ .bind(&request.body.name)
254
+ .bind(&claims.sub)
255
+ .fetch_one(&self.state.db)
256
+ .await?;
257
+ Ok(GroupsCreateResponse::Created(Json(group)))
258
+ }
259
+
260
+ // ... implement other methods
261
+ }
262
+ ```
263
+
264
+ ### Creating the Router
265
+
266
+ ```rust
267
+ // Define your claims type
268
+ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
269
+ pub struct Claims {
270
+ pub sub: String,
271
+ pub exp: usize,
272
+ }
273
+
274
+ // Create auth middleware
275
+ async fn auth_middleware(
276
+ Extension(claims): Extension<Claims>,
277
+ request: Request<axum::body::Body>,
278
+ next: Next,
279
+ ) -> Result<Response, StatusCode> {
280
+ // Validate claims (e.g., check expiration)
281
+ if claims.exp < (chrono::Utc::now().timestamp() as usize) {
282
+ return Err(StatusCode::UNAUTHORIZED);
283
+ }
284
+ Ok(next.run(request).await)
285
+ }
286
+
287
+ // Apply middleware to protected routes
288
+ fn with_auth<S>(router: Router<S>) -> Router<S>
289
+ where
290
+ S: Server + Clone + Send + Sync + 'static,
291
+ S::Claims: Send + Sync + Clone + 'static,
292
+ {
293
+ router.layer(axum::middleware::from_fn(auth_middleware))
294
+ }
295
+
296
+ // Create the router
297
+ let server = MyServer::new(state);
298
+ let router = create_router(server, with_auth);
299
+ axum::serve(listener, router).await?;
300
+ ```
301
+
302
+ ### Protected Routes
303
+
304
+ Operations decorated with `@useAuth` will:
305
+
306
+ - Receive `claims: Self::Claims` as the first parameter
307
+ - Be wrapped with the middleware passed to `create_router`
308
+
309
+ Public routes do not receive claims and are not wrapped with auth middleware.
310
+
182
311
  ## Type Mappings
183
312
 
184
313
  | TypeSpec Type | Rust Type |
@@ -193,6 +322,26 @@ pub struct GroupStatistics {
193
322
  | `Record<string>` | `std::collections::HashMap<String, String>` |
194
323
  | `T \| null` | `Option<T>` |
195
324
 
325
+ ### Format Mappings
326
+
327
+ | `@format()` | Rust Type |
328
+ | ------------- | ----------------------- |
329
+ | `"uuid"` | `uuid::Uuid` |
330
+ | `"date"` | `chrono::NaiveDate` |
331
+ | `"time"` | `chrono::NaiveTime` |
332
+ | `"date-time"` | `chrono::DateTime<Utc>` |
333
+
334
+ ## Decorators
335
+
336
+ | Decorator | Description |
337
+ | ---------------------------- | ----------------------------------------------------- |
338
+ | `@error` | Adds `thiserror::Error` derive with error message |
339
+ | `@pattern("regex")` | Generates `TryFrom<String>` with regex validation |
340
+ | `@rustDerive("...")` | Adds a custom derive macro (e.g., `sqlx::FromRow`) |
341
+ | `@rustDerives("...", "...")` | Adds multiple custom derive macros |
342
+ | `@doc("...")` | Generates `///` doc comments |
343
+ | `@useAuth(AuthType)` | Marks operation as protected, adds `Claims` parameter |
344
+
196
345
  ## Building
197
346
 
198
347
  ```bash