typespec-rust-emitter 0.2.0 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,100 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2026-03-27
9
+
10
+ ### Changed
11
+
12
+ - **Breaking**: Server trait methods now accept individual parameters instead of request structs
13
+ - `async fn subjects_create(&self, claims: Self::Claims, request: SubjectsCreateRequest)` → `async fn subjects_create(&self, claims: Self::Claims, group_id: i64, body: CreateSubjectBody)`
14
+ - Path parameters extracted with `Path<T>` extractor
15
+ - Body extracted with `Json<T>` extractor
16
+ - Request structs (`SubjectsCreateRequest`, etc.) are no longer generated
17
+ - **Breaking**: Extractor order in handlers follows axum conventions
18
+ - Order: `State` → `Extension` → `Path` → `Json`
19
+ - Fixes axum 0.8 `Handler` trait inference issues
20
+ - Removed unused `Query` extractor import from generated code
21
+ - Fixed decorator detection to use `getDecoratorName()` instead of direct property access
22
+
23
+ ### Benefits
24
+
25
+ - Cleaner server trait API - no boilerplate request structs
26
+ - Direct parameter access in handler implementations
27
+ - Follows axum best practices for extractor ordering
28
+ - Better IDE autocomplete for handler parameters
29
+
30
+ ### Example
31
+
32
+ **TypeSpec:**
33
+
34
+ ```typespec
35
+ @post
36
+ @route("/groups/{groupId}/subjects")
37
+ op create(@path groupId: int64, @body body: CreateSubjectBody): Subject;
38
+ ```
39
+
40
+ **Before (v0.3.0):**
41
+
42
+ ```rust
43
+ // Server trait
44
+ async fn subjects_create(&self, claims: Self::Claims, request: SubjectsCreateRequest) -> Result<...>;
45
+
46
+ // Handler
47
+ axum::extract::Query(query): axum::extract::Query<SubjectsCreateRequest>
48
+ let result = service.subjects_create(claims, query).await;
49
+ ```
50
+
51
+ **After (v0.4.0):**
52
+
53
+ ```rust
54
+ // Server trait
55
+ async fn subjects_create(&self, claims: Self::Claims, group_id: i64, body: CreateSubjectBody) -> Result<...>;
56
+
57
+ // Handler
58
+ Extension(claims): Extension<S::Claims>,
59
+ Path(group_id): Path<i64>,
60
+ Json(payload): Json<CreateSubjectBody>
61
+ let result = service.subjects_create(claims, group_id, payload).await;
62
+ ```
63
+
64
+ ## [0.3.0] - 2026-03-27
65
+
66
+ ### Added
67
+
68
+ - `@rustAttr` and `@rustAttrs` decorators for adding custom Rust attributes to models and enums
69
+ - Example: `@rustAttr("sqlx(type_name = \"user\")")` generates `#[sqlx(type_name = "user")]`
70
+ - Support for `@rustDerive` and `@rustDerives` on enums (previously only models)
71
+ - Enables sqlx and other derive macros on enum types
72
+
73
+ ### Changed
74
+
75
+ - Custom attributes are now rendered after `#[derive(...)]` for proper Rust syntax
76
+ - Updated example to demonstrate `@rustDerive` + `@rustAttr` combination on enums
77
+
78
+ ### Example
79
+
80
+ ```typespec
81
+ @rustDerive("sqlx::Type")
82
+ @rustAttr("sqlx(type_name = \"study_status\")")
83
+ enum StudyStatus {
84
+ Starting,
85
+ Paused,
86
+ }
87
+ ```
88
+
89
+ Generates:
90
+
91
+ ```rust
92
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, sqlx::Type)]
93
+ #[sqlx(type_name = "study_status")]
94
+ pub enum StudyStatus {
95
+ #[serde(rename = "Starting")]
96
+ Starting,
97
+ #[serde(rename = "Paused")]
98
+ Paused,
99
+ }
100
+ ```
101
+
8
102
  ## [0.2.0] - 2026-03-27
9
103
 
10
104
  ### Changed
package/QWEN.md CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  - **Type**: TypeScript/Node.js project (TypeSpec emitter)
8
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
9
+ - **Main Entry**: `src/emitter.ts` - core codegen logic
10
+ - **Package**: `typespec-rust-emitter` v0.3.0
11
11
 
12
12
  ## Directory Structure
13
13
 
@@ -15,12 +15,12 @@
15
15
  typespec-emitter/
16
16
  ├── src/
17
17
  │ ├── emitter.ts # MAIN: TypeSpec→Rust code generation
18
- │ ├── index.ts # Exports ($onEmit, $rustDerive, $rustDerives)
18
+ │ ├── index.ts # Exports ($onEmit, $rustDerive, $rustDerives, $rustAttr, $rustAttrs)
19
19
  │ ├── lib.ts # TypeSpec library definition
20
20
  │ ├── lib.tsp # Decorator declarations
21
21
  │ └── testing/ # Test utilities
22
22
  ├── test/
23
- │ ├── hello.test.ts # Unit tests (11 tests)
23
+ │ ├── hello.test.ts # Unit tests
24
24
  │ └── test-host.ts # Test infrastructure (emit() helper)
25
25
  ├── example/
26
26
  │ ├── main.tsp # Demo TypeSpec entry
@@ -37,6 +37,7 @@ typespec-emitter/
37
37
  ## Build & Run Commands
38
38
 
39
39
  ### Primary Workflow (Run After Every Change)
40
+
40
41
  ```bash
41
42
  just build # Compile TypeScript (npm run build)
42
43
  just test # Run unit tests (npm test)
@@ -45,6 +46,7 @@ just check-rust # Verify Rust compiles (cargo check)
45
46
  ```
46
47
 
47
48
  ### Individual Commands
49
+
48
50
  ```bash
49
51
  # Development
50
52
  npm run build # tsc compile
@@ -61,47 +63,52 @@ just publish # build + npm publish
61
63
 
62
64
  ## Type Mappings (TypeSpec → Rust)
63
65
 
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>` |
66
+ | TypeSpec | Rust |
67
+ | ---------------- | -------------------- |
68
+ | `string` | `String` |
69
+ | `int8/16/32/64` | `i8/i16/i32/i64` |
70
+ | `uint8/16/32/64` | `u8/u16/u32/u64` |
71
+ | `float32/64` | `f32/f64` |
72
+ | `boolean` | `bool` |
73
+ | `bytes` | `Vec<u8>` |
74
+ | `T[]` | `Vec<T>` |
75
+ | `Record<T>` | `HashMap<String, T>` |
76
+ | `T \| null` | `Option<T>` |
75
77
 
76
78
  ### Format Mappings
77
- | `@format()` | Rust Type |
78
- |-------------|-----------|
79
- | `"uuid"` | `uuid::Uuid` |
80
- | `"date"` | `chrono::NaiveDate` |
81
- | `"time"` | `chrono::NaiveTime` |
79
+
80
+ | `@format()` | Rust Type |
81
+ | ------------- | ----------------------- |
82
+ | `"uuid"` | `uuid::Uuid` |
83
+ | `"date"` | `chrono::NaiveDate` |
84
+ | `"time"` | `chrono::NaiveTime` |
82
85
  | `"date-time"` | `chrono::DateTime<Utc>` |
83
86
 
84
87
  ## Decorators
85
88
 
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 |
89
+ | Decorator | Effect |
90
+ | ---------------------------- | ---------------------------------------------------------------------------- |
91
+ | `@error` | Adds `thiserror::Error` derive + `#[error("{code}: {message}")]` |
92
+ | `@pattern("regex")` | Generates `TryFrom<String>` with regex validation |
93
+ | `@rustDerive("...")` | Adds custom derive (models & enums, e.g., `sqlx::FromRow`, `sqlx::Type`) |
94
+ | `@rustDerives("...", "...")` | Adds multiple custom derives (models & enums) |
95
+ | `@rustAttr("...")` | Adds custom Rust attribute (models & enums, e.g., `sqlx(type_name = "...")`) |
96
+ | `@rustAttrs("...", "...")` | Adds multiple custom attributes (models & enums) |
97
+ | `@doc("...")` | Generates `///` doc comments |
98
+ | `@useAuth(BearerAuth)` | Marks operation as protected (adds `Claims` param) |
99
+ | `@maxLength(n)` | Documentation only |
95
100
 
96
101
  ## Output Files
97
102
 
98
103
  ### types.rs
104
+
99
105
  - All models → `pub struct` with serde derives
100
106
  - Enums → `pub enum` with serde rename
101
107
  - Unions → `Option<T>` for nullable, or sum types
102
108
  - Scalars → `pub type` or `pub struct` (if pattern validation)
103
109
 
104
110
  ### server.rs
111
+
105
112
  - `Server` trait with `type Claims` associated type
106
113
  - Request structs per operation
107
114
  - Response enums with status code variants
@@ -110,6 +117,7 @@ just publish # build + npm publish
110
117
  ## Key Implementation Details
111
118
 
112
119
  ### emitter.ts Architecture
120
+
113
121
  1. `navigateProgram()` - Walks TypeSpec AST
114
122
  2. Collects: models, enums, unions, scalars, operations
115
123
  3. Processes decorators (`@error`, `@pattern`, `@rustDerive`)
@@ -117,15 +125,19 @@ just publish # build + npm publish
117
125
  5. Emits files to configured output directory
118
126
 
119
127
  ### Server Trait Generation
128
+
120
129
  - Generates `Server` trait with `type Claims: Send + Sync + 'static` associated type
121
- - Handler functions: `pub async fn {op}_handler<S>(...)` with `<S>` generics only
130
+ - Handler functions accept individual parameters (path params, body) directly
131
+ - Path params extracted with `Path<T>`
132
+ - Body extracted with `Json<T>`
122
133
  - Protected routes (`@useAuth`) receive `claims: Self::Claims` via `Extension<Self::Claims>`
123
- - Public routes don't require claims
134
+ - Extractor order: `State` `Extension` → `Path` → `Json`
124
135
  - `create_router<S, M>(service: S, middleware: M)` accepts middleware function
125
136
  - Middleware wraps protected routes: `middleware(protected)`
126
137
  - Router merges public/protected, applies `.with_state(service)` once at end
127
138
 
128
139
  ### Test Pattern
140
+
129
141
  ```typescript
130
142
  import { emit } from "./test-host.js";
131
143
  const results = await emit(`model User { name: string; }`);
@@ -136,15 +148,18 @@ strictEqual(output.includes("pub struct User"), true);
136
148
  ## Dependencies
137
149
 
138
150
  ### Peer (Required)
151
+
139
152
  - `@typespec/compiler` >=1.0.0
140
153
  - `@typespec/emitter-framework` ^0.17.0
141
154
 
142
155
  ### Dev
156
+
143
157
  - `typescript` ^5.3.3
144
158
  - `eslint` ^9.15.0
145
159
  - `prettier` ^3.3.3
146
160
 
147
161
  ### Generated Rust (example/output-rust/Cargo.toml)
162
+
148
163
  ```toml
149
164
  serde = { version = "1.0", features = ["derive"] }
150
165
  thiserror = "2.0"
@@ -161,6 +176,7 @@ tokio = { version = "1.50", features = ["full"] }
161
176
  ## Configuration Files
162
177
 
163
178
  ### tspconfig.yaml
179
+
164
180
  ```yaml
165
181
  emit:
166
182
  - "@typespec/openapi3"
@@ -171,6 +187,7 @@ options:
171
187
  ```
172
188
 
173
189
  ### tsconfig.json
190
+
174
191
  - Target: ES2022
175
192
  - Module: NodeNext
176
193
  - Strict: true
@@ -194,9 +211,67 @@ options:
194
211
 
195
212
  ## Recent Changes
196
213
 
214
+ ### 2026-03-27: v0.4.0 - Individual Parameters Instead of Request Structs
215
+
216
+ **Breaking Changes**:
217
+
218
+ - Server trait methods now accept individual parameters instead of request structs
219
+ - `async fn subjects_create(&self, claims: Self::Claims, request: SubjectsCreateRequest)` → `async fn subjects_create(&self, claims: Self::Claims, group_id: i64, body: CreateSubjectBody)`
220
+ - Request structs (`SubjectsCreateRequest`, etc.) are no longer generated
221
+ - Handler extractor order follows axum conventions: `State` → `Extension` → `Path` → `Json`
222
+
223
+ **Benefits**:
224
+
225
+ - Cleaner server trait API - no boilerplate request structs
226
+ - Direct parameter access in handler implementations
227
+ - Follows axum best practices for extractor ordering
228
+ - Better IDE autocomplete for handler parameters
229
+
230
+ **Example**:
231
+
232
+ ```typespec
233
+ @post
234
+ @route("/groups/{groupId}/subjects")
235
+ op create(@path groupId: int64, @body body: CreateSubjectBody): Subject;
236
+ ```
237
+
238
+ ```rust
239
+ // Server trait
240
+ async fn subjects_create(&self, claims: Self::Claims, group_id: i64, body: CreateSubjectBody) -> Result<...>;
241
+
242
+ // Handler
243
+ Extension(claims): Extension<S::Claims>,
244
+ Path(group_id): Path<i64>,
245
+ Json(payload): Json<CreateSubjectBody>
246
+ let result = service.subjects_create(claims, group_id, payload).await;
247
+ ```
248
+
249
+ ### 2026-03-27: v0.3.0 - Custom Attributes & Enum Derives
250
+
251
+ **New Features**:
252
+
253
+ - `@rustAttr` / `@rustAttrs` decorators for custom Rust attributes (models & enums)
254
+ - `@rustDerive` / `@rustDerives` now work on enums (previously models only)
255
+ - Attributes render after `#[derive(...)]` for proper Rust syntax
256
+
257
+ **Example**:
258
+
259
+ ```typespec
260
+ @rustDerive("sqlx::Type")
261
+ @rustAttr("sqlx(type_name = \"study_status\")")
262
+ enum StudyStatus { Starting, Paused }
263
+ ```
264
+
265
+ ```rust
266
+ #[derive(..., sqlx::Type)]
267
+ #[sqlx(type_name = "study_status")]
268
+ pub enum StudyStatus { ... }
269
+ ```
270
+
197
271
  ### 2026-03-27: v0.2.0 - Server Trait Refactoring
198
272
 
199
273
  **Changes**:
274
+
200
275
  - `pub trait Server<Claims>` → `pub trait Server` with `type Claims` associated type
201
276
  - Removed generated `Claims` struct from `types.rs` (users define their own)
202
277
  - `create_router<S, M>(service: S, middleware: M)` now accepts middleware function
@@ -204,6 +279,7 @@ options:
204
279
  - Single `.with_state(service)` call at end of router creation
205
280
 
206
281
  **Benefits**:
282
+
207
283
  - Cleaner API - Claims type defined by user, not generated
208
284
  - Flexible middleware - any function `FnOnce(Router<S>) -> Router<S>`
209
285
  - No duplicate `.with_state(service.clone())` calls
package/README.md CHANGED
@@ -18,7 +18,8 @@ A TypeSpec emitter that generates idiomatic Rust types and structs from TypeSpec
18
18
  - **Inheritance**: Supports model inheritance with `getAllProperties()`
19
19
  - **Error Models**: Generates `thiserror::Error` derive with `#[error(...)]` attributes
20
20
  - **Pattern Validation**: Supports `@pattern` decorators with `TryFrom<String>` validation
21
- - **Custom Derives**: Add arbitrary Rust derive macros via `@rustDerive` decorator
21
+ - **Custom Derives**: Add arbitrary Rust derive macros via `@rustDerive` decorator (models & enums)
22
+ - **Custom Attributes**: Add arbitrary Rust attributes via `@rustAttr` decorator (models & enums)
22
23
  - **Server Trait**: Generates axum server trait and router from HTTP operations
23
24
 
24
25
  ## Installation
@@ -178,6 +179,44 @@ pub struct GroupStatistics {
178
179
  }
179
180
  ```
180
181
 
182
+ ### Custom Attributes
183
+
184
+ ```typespec
185
+ import "typespec-emitter";
186
+
187
+ @rustAttr("sqlx(type_name = \"user\")")
188
+ model User {
189
+ name: string;
190
+ }
191
+
192
+ @rustDerive("sqlx::Type")
193
+ @rustAttr("sqlx(type_name = \"study_status\")")
194
+ enum StudyStatus {
195
+ Starting,
196
+ Paused,
197
+ }
198
+ ```
199
+
200
+ Generates:
201
+
202
+ ```rust
203
+ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
204
+ #[sqlx(type_name = "user")]
205
+ pub struct User {
206
+ #[serde(rename = "name")]
207
+ pub name: String,
208
+ }
209
+
210
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, sqlx::Type)]
211
+ #[sqlx(type_name = "study_status")]
212
+ pub enum StudyStatus {
213
+ #[serde(rename = "Starting")]
214
+ Starting,
215
+ #[serde(rename = "Paused")]
216
+ Paused,
217
+ }
218
+ ```
219
+
181
220
  ## Server Trait Generation
182
221
 
183
222
  The emitter can generate a complete axum server trait and router from TypeSpec HTTP operations.
@@ -209,9 +248,9 @@ namespace Groups {
209
248
  pub trait Server: Send + Sync {
210
249
  type Claims: Send + Sync + 'static;
211
250
 
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>;
251
+ async fn groups_list(&self) -> Result<GroupsListResponse>;
252
+ async fn groups_create(&self, claims: Self::Claims, body: CreateGroupBody) -> Result<GroupsCreateResponse>;
253
+ async fn groups_get_by_id(&self, id: i64) -> Result<GroupsGetByIdResponse>;
215
254
  }
216
255
  ```
217
256
 
@@ -231,10 +270,7 @@ pub struct MyServer {
231
270
  impl Server for MyServer {
232
271
  type Claims = Claims; // Your custom claims type
233
272
 
234
- async fn groups_list(
235
- &self,
236
- _request: GroupsListRequest,
237
- ) -> eyre::Result<GroupsListResponse> {
273
+ async fn groups_list(&self) -> eyre::Result<GroupsListResponse> {
238
274
  let groups = sqlx::query_as::<_, Group>("SELECT * FROM groups")
239
275
  .fetch_all(&self.state.db)
240
276
  .await?;
@@ -244,19 +280,29 @@ impl Server for MyServer {
244
280
  async fn groups_create(
245
281
  &self,
246
282
  claims: Self::Claims,
247
- request: GroupsCreateRequest,
283
+ body: CreateGroupBody,
248
284
  ) -> eyre::Result<GroupsCreateResponse> {
249
- // claims.sub contains the authenticated user ID
285
+ // Direct access to body fields
250
286
  let group = sqlx::query_as::<_, Group>(
251
287
  "INSERT INTO groups (name, owner_id) VALUES ($1, $2) RETURNING *"
252
288
  )
253
- .bind(&request.body.name)
289
+ .bind(&body.name)
254
290
  .bind(&claims.sub)
255
291
  .fetch_one(&self.state.db)
256
292
  .await?;
257
293
  Ok(GroupsCreateResponse::Created(Json(group)))
258
294
  }
259
295
 
296
+ async fn groups_get_by_id(&self, id: i64) -> eyre::Result<GroupsGetByIdResponse> {
297
+ // Direct access to path parameter
298
+ let group = sqlx::query_as::<_, Group>("SELECT * FROM groups WHERE id = $1")
299
+ .bind(id)
300
+ .fetch_optional(&self.state.db)
301
+ .await?
302
+ .ok_or_else(|| GroupsGetByIdResponse::NotFound)?;
303
+ Ok(GroupsGetByIdResponse::Ok(Json(group)))
304
+ }
305
+
260
306
  // ... implement other methods
261
307
  }
262
308
  ```
@@ -337,8 +383,10 @@ Public routes do not receive claims and are not wrapped with auth middleware.
337
383
  | ---------------------------- | ----------------------------------------------------- |
338
384
  | `@error` | Adds `thiserror::Error` derive with error message |
339
385
  | `@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 |
386
+ | `@rustDerive("...")` | Adds a custom derive macro (models & enums) |
387
+ | `@rustDerives("...", "...")` | Adds multiple custom derive macros (models & enums) |
388
+ | `@rustAttr("...")` | Adds a custom Rust attribute (models & enums) |
389
+ | `@rustAttrs("...", "...")` | Adds multiple custom Rust attributes (models & enums) |
342
390
  | `@doc("...")` | Generates `///` doc comments |
343
391
  | `@useAuth(AuthType)` | Marks operation as protected, adds `Claims` parameter |
344
392
 
@@ -4,4 +4,6 @@ export interface RustEmitterOptions {
4
4
  }
5
5
  export declare function $rustDerive(context: DecoratorContext, target: Type, derive: string): void;
6
6
  export declare function $rustDerives(context: DecoratorContext, target: Type, ...derives: string[]): void;
7
+ export declare function $rustAttr(context: DecoratorContext, target: Type, attr: string): void;
8
+ export declare function $rustAttrs(context: DecoratorContext, target: Type, ...attrs: string[]): void;
7
9
  export declare function $onEmit(context: EmitContext<RustEmitterOptions>, _options?: RustEmitterOptions): Promise<void>;