typespec-rust-emitter 0.3.0 → 0.5.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,95 @@ 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.5.0] - 2026-03-29
9
+
10
+ ### Changed
11
+
12
+ - Date/time scalars now generate proper chrono types instead of `String`:
13
+ - `utcDateTime` → `chrono::DateTime<chrono::Utc>`
14
+ - `offsetDateTime` → `chrono::DateTime<chrono::FixedOffset>`
15
+ - `plainDateTime` → `chrono::NaiveDateTime`
16
+ - `plainDate` → `chrono::NaiveDate`
17
+ - `plainTime` → `chrono::NaiveTime`
18
+
19
+ ### Example
20
+
21
+ ```typespec
22
+ model Event {
23
+ createdAt: utcDateTime;
24
+ startTime: plainDateTime;
25
+ eventDate: plainDate;
26
+ eventTime: plainTime;
27
+ }
28
+ ```
29
+
30
+ Generates:
31
+
32
+ ```rust
33
+ pub struct Event {
34
+ pub created_at: chrono::DateTime<chrono::Utc>,
35
+ pub start_time: chrono::NaiveDateTime,
36
+ pub event_date: chrono::NaiveDate,
37
+ pub event_time: chrono::NaiveTime,
38
+ }
39
+ ```
40
+
41
+ ## [0.4.0] - 2026-03-27
42
+
43
+ ### Changed
44
+
45
+ - **Breaking**: Server trait methods now accept individual parameters instead of request structs
46
+ - `async fn subjects_create(&self, claims: Self::Claims, request: SubjectsCreateRequest)` → `async fn subjects_create(&self, claims: Self::Claims, group_id: i64, body: CreateSubjectBody)`
47
+ - Path parameters extracted with `Path<T>` extractor
48
+ - Body extracted with `Json<T>` extractor
49
+ - Request structs (`SubjectsCreateRequest`, etc.) are no longer generated
50
+ - **Breaking**: Extractor order in handlers follows axum conventions
51
+ - Order: `State` → `Extension` → `Path` → `Json`
52
+ - Fixes axum 0.8 `Handler` trait inference issues
53
+ - Removed unused `Query` extractor import from generated code
54
+ - Fixed decorator detection to use `getDecoratorName()` instead of direct property access
55
+
56
+ ### Benefits
57
+
58
+ - Cleaner server trait API - no boilerplate request structs
59
+ - Direct parameter access in handler implementations
60
+ - Follows axum best practices for extractor ordering
61
+ - Better IDE autocomplete for handler parameters
62
+
63
+ ### Example
64
+
65
+ **TypeSpec:**
66
+
67
+ ```typespec
68
+ @post
69
+ @route("/groups/{groupId}/subjects")
70
+ op create(@path groupId: int64, @body body: CreateSubjectBody): Subject;
71
+ ```
72
+
73
+ **Before (v0.3.0):**
74
+
75
+ ```rust
76
+ // Server trait
77
+ async fn subjects_create(&self, claims: Self::Claims, request: SubjectsCreateRequest) -> Result<...>;
78
+
79
+ // Handler
80
+ axum::extract::Query(query): axum::extract::Query<SubjectsCreateRequest>
81
+ let result = service.subjects_create(claims, query).await;
82
+ ```
83
+
84
+ **After (v0.4.0):**
85
+
86
+ ```rust
87
+ // Server trait
88
+ async fn subjects_create(&self, claims: Self::Claims, group_id: i64, body: CreateSubjectBody) -> Result<...>;
89
+
90
+ // Handler
91
+ Extension(claims): Extension<S::Claims>,
92
+ Path(group_id): Path<i64>,
93
+ Json(payload): Json<CreateSubjectBody>
94
+ let result = service.subjects_create(claims, group_id, payload).await;
95
+ ```
96
+
8
97
  ## [0.3.0] - 2026-03-27
9
98
 
10
99
  ### Added
package/QWEN.md CHANGED
@@ -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,49 +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 (models & enums, e.g., `sqlx::FromRow`, `sqlx::Type`) |
91
- | `@rustDerives("...", "...")` | Adds multiple custom derives (models & enums) |
92
- | `@rustAttr("...")` | Adds custom Rust attribute (models & enums, e.g., `sqlx(type_name = "...")`) |
93
- | `@rustAttrs("...", "...")` | Adds multiple custom attributes (models & enums) |
94
- | `@doc("...")` | Generates `///` doc comments |
95
- | `@useAuth(BearerAuth)` | Marks operation as protected (adds `Claims` param) |
96
- | `@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 |
97
100
 
98
101
  ## Output Files
99
102
 
100
103
  ### types.rs
104
+
101
105
  - All models → `pub struct` with serde derives
102
106
  - Enums → `pub enum` with serde rename
103
107
  - Unions → `Option<T>` for nullable, or sum types
104
108
  - Scalars → `pub type` or `pub struct` (if pattern validation)
105
109
 
106
110
  ### server.rs
111
+
107
112
  - `Server` trait with `type Claims` associated type
108
113
  - Request structs per operation
109
114
  - Response enums with status code variants
@@ -112,6 +117,7 @@ just publish # build + npm publish
112
117
  ## Key Implementation Details
113
118
 
114
119
  ### emitter.ts Architecture
120
+
115
121
  1. `navigateProgram()` - Walks TypeSpec AST
116
122
  2. Collects: models, enums, unions, scalars, operations
117
123
  3. Processes decorators (`@error`, `@pattern`, `@rustDerive`)
@@ -119,15 +125,19 @@ just publish # build + npm publish
119
125
  5. Emits files to configured output directory
120
126
 
121
127
  ### Server Trait Generation
128
+
122
129
  - Generates `Server` trait with `type Claims: Send + Sync + 'static` associated type
123
- - 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>`
124
133
  - Protected routes (`@useAuth`) receive `claims: Self::Claims` via `Extension<Self::Claims>`
125
- - Public routes don't require claims
134
+ - Extractor order: `State` `Extension` → `Path` → `Json`
126
135
  - `create_router<S, M>(service: S, middleware: M)` accepts middleware function
127
136
  - Middleware wraps protected routes: `middleware(protected)`
128
137
  - Router merges public/protected, applies `.with_state(service)` once at end
129
138
 
130
139
  ### Test Pattern
140
+
131
141
  ```typescript
132
142
  import { emit } from "./test-host.js";
133
143
  const results = await emit(`model User { name: string; }`);
@@ -138,15 +148,18 @@ strictEqual(output.includes("pub struct User"), true);
138
148
  ## Dependencies
139
149
 
140
150
  ### Peer (Required)
151
+
141
152
  - `@typespec/compiler` >=1.0.0
142
153
  - `@typespec/emitter-framework` ^0.17.0
143
154
 
144
155
  ### Dev
156
+
145
157
  - `typescript` ^5.3.3
146
158
  - `eslint` ^9.15.0
147
159
  - `prettier` ^3.3.3
148
160
 
149
161
  ### Generated Rust (example/output-rust/Cargo.toml)
162
+
150
163
  ```toml
151
164
  serde = { version = "1.0", features = ["derive"] }
152
165
  thiserror = "2.0"
@@ -163,6 +176,7 @@ tokio = { version = "1.50", features = ["full"] }
163
176
  ## Configuration Files
164
177
 
165
178
  ### tspconfig.yaml
179
+
166
180
  ```yaml
167
181
  emit:
168
182
  - "@typespec/openapi3"
@@ -173,6 +187,7 @@ options:
173
187
  ```
174
188
 
175
189
  ### tsconfig.json
190
+
176
191
  - Target: ES2022
177
192
  - Module: NodeNext
178
193
  - Strict: true
@@ -196,14 +211,51 @@ options:
196
211
 
197
212
  ## Recent Changes
198
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
+
199
249
  ### 2026-03-27: v0.3.0 - Custom Attributes & Enum Derives
200
250
 
201
251
  **New Features**:
252
+
202
253
  - `@rustAttr` / `@rustAttrs` decorators for custom Rust attributes (models & enums)
203
254
  - `@rustDerive` / `@rustDerives` now work on enums (previously models only)
204
255
  - Attributes render after `#[derive(...)]` for proper Rust syntax
205
256
 
206
257
  **Example**:
258
+
207
259
  ```typespec
208
260
  @rustDerive("sqlx::Type")
209
261
  @rustAttr("sqlx(type_name = \"study_status\")")
@@ -219,6 +271,7 @@ pub enum StudyStatus { ... }
219
271
  ### 2026-03-27: v0.2.0 - Server Trait Refactoring
220
272
 
221
273
  **Changes**:
274
+
222
275
  - `pub trait Server<Claims>` → `pub trait Server` with `type Claims` associated type
223
276
  - Removed generated `Claims` struct from `types.rs` (users define their own)
224
277
  - `create_router<S, M>(service: S, middleware: M)` now accepts middleware function
@@ -226,6 +279,7 @@ pub enum StudyStatus { ... }
226
279
  - Single `.with_state(service)` call at end of router creation
227
280
 
228
281
  **Benefits**:
282
+
229
283
  - Cleaner API - Claims type defined by user, not generated
230
284
  - Flexible middleware - any function `FnOnce(Router<S>) -> Router<S>`
231
285
  - No duplicate `.with_state(service.clone())` calls
package/README.md CHANGED
@@ -248,9 +248,9 @@ namespace Groups {
248
248
  pub trait Server: Send + Sync {
249
249
  type Claims: Send + Sync + 'static;
250
250
 
251
- async fn groups_list(&self, request: GroupsListRequest) -> Result<GroupsListResponse>;
252
- async fn groups_create(&self, claims: Self::Claims, request: GroupsCreateRequest) -> Result<GroupsCreateResponse>;
253
- 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>;
254
254
  }
255
255
  ```
256
256
 
@@ -270,10 +270,7 @@ pub struct MyServer {
270
270
  impl Server for MyServer {
271
271
  type Claims = Claims; // Your custom claims type
272
272
 
273
- async fn groups_list(
274
- &self,
275
- _request: GroupsListRequest,
276
- ) -> eyre::Result<GroupsListResponse> {
273
+ async fn groups_list(&self) -> eyre::Result<GroupsListResponse> {
277
274
  let groups = sqlx::query_as::<_, Group>("SELECT * FROM groups")
278
275
  .fetch_all(&self.state.db)
279
276
  .await?;
@@ -283,19 +280,29 @@ impl Server for MyServer {
283
280
  async fn groups_create(
284
281
  &self,
285
282
  claims: Self::Claims,
286
- request: GroupsCreateRequest,
283
+ body: CreateGroupBody,
287
284
  ) -> eyre::Result<GroupsCreateResponse> {
288
- // claims.sub contains the authenticated user ID
285
+ // Direct access to body fields
289
286
  let group = sqlx::query_as::<_, Group>(
290
287
  "INSERT INTO groups (name, owner_id) VALUES ($1, $2) RETURNING *"
291
288
  )
292
- .bind(&request.body.name)
289
+ .bind(&body.name)
293
290
  .bind(&claims.sub)
294
291
  .fetch_one(&self.state.db)
295
292
  .await?;
296
293
  Ok(GroupsCreateResponse::Created(Json(group)))
297
294
  }
298
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
+
299
306
  // ... implement other methods
300
307
  }
301
308
  ```
@@ -352,14 +359,19 @@ Public routes do not receive claims and are not wrapped with auth middleware.
352
359
  | TypeSpec Type | Rust Type |
353
360
  | ---------------- | ------------------------------------------- |
354
361
  | `string` | `String` |
355
- | `int32` | `i32` |
356
- | `int64` | `i64` |
357
- | `float32` | `f32` |
358
- | `float64` | `f64` |
362
+ | `int8/16/32/64` | `i8/i16/i32/i64` |
363
+ | `uint8/16/32/64` | `u8/u16/u32/u64` |
364
+ | `float32/64` | `f32/f64` |
359
365
  | `boolean` | `bool` |
366
+ | `bytes` | `Vec<u8>` |
360
367
  | `string[]` | `Vec<String>` |
361
368
  | `Record<string>` | `std::collections::HashMap<String, String>` |
362
369
  | `T \| null` | `Option<T>` |
370
+ | `utcDateTime` | `chrono::DateTime<chrono::Utc>` |
371
+ | `offsetDateTime` | `chrono::DateTime<chrono::FixedOffset>` |
372
+ | `plainDateTime` | `chrono::NaiveDateTime` |
373
+ | `plainDate` | `chrono::NaiveDate` |
374
+ | `plainTime` | `chrono::NaiveTime` |
363
375
 
364
376
  ### Format Mappings
365
377
 
@@ -372,16 +384,16 @@ Public routes do not receive claims and are not wrapped with auth middleware.
372
384
 
373
385
  ## Decorators
374
386
 
375
- | Decorator | Description |
376
- | ---------------------------- | ---------------------------------------------------------------- |
377
- | `@error` | Adds `thiserror::Error` derive with error message |
378
- | `@pattern("regex")` | Generates `TryFrom<String>` with regex validation |
379
- | `@rustDerive("...")` | Adds a custom derive macro (models & enums) |
380
- | `@rustDerives("...", "...")` | Adds multiple custom derive macros (models & enums) |
381
- | `@rustAttr("...")` | Adds a custom Rust attribute (models & enums) |
382
- | `@rustAttrs("...", "...")` | Adds multiple custom Rust attributes (models & enums) |
383
- | `@doc("...")` | Generates `///` doc comments |
384
- | `@useAuth(AuthType)` | Marks operation as protected, adds `Claims` parameter |
387
+ | Decorator | Description |
388
+ | ---------------------------- | ----------------------------------------------------- |
389
+ | `@error` | Adds `thiserror::Error` derive with error message |
390
+ | `@pattern("regex")` | Generates `TryFrom<String>` with regex validation |
391
+ | `@rustDerive("...")` | Adds a custom derive macro (models & enums) |
392
+ | `@rustDerives("...", "...")` | Adds multiple custom derive macros (models & enums) |
393
+ | `@rustAttr("...")` | Adds a custom Rust attribute (models & enums) |
394
+ | `@rustAttrs("...", "...")` | Adds multiple custom Rust attributes (models & enums) |
395
+ | `@doc("...")` | Generates `///` doc comments |
396
+ | `@useAuth(AuthType)` | Marks operation as protected, adds `Claims` parameter |
385
397
 
386
398
  ## Building
387
399