typespec-rust-emitter 0.3.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,62 @@ 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
+
8
64
  ## [0.3.0] - 2026-03-27
9
65
 
10
66
  ### 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
  ```
@@ -372,16 +379,16 @@ Public routes do not receive claims and are not wrapped with auth middleware.
372
379
 
373
380
  ## Decorators
374
381
 
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 |
382
+ | Decorator | Description |
383
+ | ---------------------------- | ----------------------------------------------------- |
384
+ | `@error` | Adds `thiserror::Error` derive with error message |
385
+ | `@pattern("regex")` | Generates `TryFrom<String>` with regex validation |
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) |
390
+ | `@doc("...")` | Generates `///` doc comments |
391
+ | `@useAuth(AuthType)` | Marks operation as protected, adds `Claims` parameter |
385
392
 
386
393
  ## Building
387
394
 
@@ -149,24 +149,46 @@ function getOperationParameters(program, operation, anonymousEnums) {
149
149
  const model = operation.parameters;
150
150
  for (const [propName, prop] of model.properties) {
151
151
  const decorators = prop.decorators;
152
+ // Skip body parameters - they are handled separately
153
+ let isBody = false;
154
+ if (decorators) {
155
+ for (const key of Object.keys(decorators)) {
156
+ const decorator = decorators[key];
157
+ const name = getDecoratorName(decorator);
158
+ if (name === "body" || name === "bodyRoot") {
159
+ isBody = true;
160
+ break;
161
+ }
162
+ }
163
+ }
164
+ if (isBody)
165
+ continue;
152
166
  let location = "query";
153
167
  let rustName = toRustIdent(propName);
154
168
  if (decorators) {
155
- if (decorators["$path"]) {
156
- location = "path";
157
- }
158
- else if (decorators["$query"]) {
159
- location = "query";
160
- }
161
- else if (decorators["$header"]) {
162
- location = "header";
163
- const headerDec = decorators["$header"];
164
- if (headerDec?.value) {
165
- rustName = headerDec.value;
169
+ for (const key of Object.keys(decorators)) {
170
+ const decorator = decorators[key];
171
+ const name = getDecoratorName(decorator);
172
+ if (name === "path") {
173
+ location = "path";
174
+ break;
175
+ }
176
+ else if (name === "query") {
177
+ location = "query";
178
+ break;
179
+ }
180
+ else if (name === "header") {
181
+ location = "header";
182
+ const headerVal = getDecoratorArgValue(decorator, 0);
183
+ if (headerVal) {
184
+ rustName = toRustIdent(headerVal);
185
+ }
186
+ break;
187
+ }
188
+ else if (name === "cookie") {
189
+ location = "cookie";
190
+ break;
166
191
  }
167
- }
168
- else if (decorators["$cookie"]) {
169
- location = "cookie";
170
192
  }
171
193
  }
172
194
  const { type: rustType } = getRustTypeForProperty(prop.type, program, anonymousEnums);
@@ -184,8 +206,14 @@ function getOperationParameters(program, operation, anonymousEnums) {
184
206
  function getOperationBody(operation) {
185
207
  for (const [_propName, prop] of operation.parameters.properties) {
186
208
  const decorators = prop.decorators;
187
- if (decorators?.["$body"] || decorators?.["$bodyRoot"]) {
188
- return prop;
209
+ if (!decorators)
210
+ continue;
211
+ for (const key of Object.keys(decorators)) {
212
+ const decorator = decorators[key];
213
+ const name = getDecoratorName(decorator);
214
+ if (name === "body" || name === "bodyRoot") {
215
+ return prop;
216
+ }
189
217
  }
190
218
  }
191
219
  return undefined;
@@ -350,6 +378,8 @@ function generateServerTrait(program, namespaceGroups, anonymousEnums) {
350
378
  parts.push(`use super::types::*;
351
379
  use async_trait::async_trait;
352
380
  use axum::{http::StatusCode, Json};
381
+ use axum::extract::Path;
382
+ use axum::Extension;
353
383
  use eyre::Result;
354
384
 
355
385
  #[async_trait]
@@ -366,53 +396,44 @@ pub trait Server: Send + Sync {
366
396
  if (opInfo.doc) {
367
397
  parts.push(` ${formatDoc(opInfo.doc)}`);
368
398
  }
369
- const requestName = `${nsName}${toPascalCase(opInfo.name)}Request`;
370
399
  const responseName = `${nsName}${toPascalCase(opInfo.name)}Response`;
371
400
  const fnName = toRustIdent(`${nsName}_${opInfo.name}`);
372
401
  const isProtected = hasAuthDecorator(op);
402
+ // Build parameter list for the trait method
403
+ const paramParts = [];
404
+ // Add path parameters
405
+ for (const param of opInfo.parameters) {
406
+ if (param.location === "path") {
407
+ paramParts.push(`${param.rustName}: ${param.rustType}`);
408
+ }
409
+ }
410
+ // Add body parameter
411
+ if (opInfo.body) {
412
+ const bodyType = getRustTypeForProperty(opInfo.body.type, program, anonymousEnums);
413
+ paramParts.push(`body: ${bodyType.type}`);
414
+ }
415
+ const paramsStr = paramParts.join(", ");
373
416
  if (isProtected) {
374
- parts.push(` async fn ${fnName}(&self, claims: Self::Claims, request: ${requestName}) -> Result<${responseName}>;`);
417
+ if (paramsStr) {
418
+ parts.push(` async fn ${fnName}(&self, claims: Self::Claims, ${paramsStr}) -> Result<${responseName}>;`);
419
+ }
420
+ else {
421
+ parts.push(` async fn ${fnName}(&self, claims: Self::Claims) -> Result<${responseName}>;`);
422
+ }
375
423
  }
376
424
  else {
377
- parts.push(` async fn ${fnName}(&self, request: ${requestName}) -> Result<${responseName}>;`);
425
+ if (paramsStr) {
426
+ parts.push(` async fn ${fnName}(&self, ${paramsStr}) -> Result<${responseName}>;`);
427
+ }
428
+ else {
429
+ parts.push(` async fn ${fnName}(&self) -> Result<${responseName}>;`);
430
+ }
378
431
  }
379
432
  }
380
433
  }
381
434
  parts.push("}");
382
435
  return parts.join("\n");
383
436
  }
384
- function generateRequestStructs(program, namespaceGroups, anonymousEnums) {
385
- const parts = [];
386
- for (const group of namespaceGroups) {
387
- const nsName = toPascalCase(group.namespace.name.replace(/[^a-zA-Z0-9_]/g, "_"));
388
- for (const op of group.operations) {
389
- const opInfo = emitOperationInfo(program, op, "", anonymousEnums);
390
- if (!opInfo)
391
- continue;
392
- const params = opInfo.parameters;
393
- const requestName = `${nsName}${toPascalCase(opInfo.name)}Request`;
394
- const fields = [];
395
- for (const param of params) {
396
- const rustType = param.optional
397
- ? `Option<${param.rustType}>`
398
- : param.rustType;
399
- fields.push(` #[serde(rename = "${param.name}", flatten)]`);
400
- fields.push(` pub ${param.rustName}: ${rustType},`);
401
- }
402
- if (opInfo.body) {
403
- const bodyType = getRustTypeForProperty(opInfo.body.type, program, anonymousEnums);
404
- fields.push(` #[serde(rename = "body")]`);
405
- fields.push(` pub body: ${bodyType.type},`);
406
- }
407
- parts.push(`#[derive(Debug, Clone, serde::Deserialize)]
408
- pub struct ${requestName} {
409
- ${fields.join("\n")}
410
- }
411
- `);
412
- }
413
- }
414
- return parts.join("\n");
415
- }
416
437
  function generateResponseEnums(program, namespaceGroups, anonymousEnums) {
417
438
  const parts = [];
418
439
  for (const group of namespaceGroups) {
@@ -478,71 +499,45 @@ function generateRouter(program, namespaceGroups, anonymousEnums) {
478
499
  usedMethods.add(method);
479
500
  const handlerFnName = toRustIdent(`${nsName}_${opInfo.name}`);
480
501
  const traitFnName = handlerFnName;
481
- const requestName = `${nsName}${toPascalCase(opInfo.name)}Request`;
482
502
  const isProtected = hasAuthDecorator(op);
483
503
  const pathParams = opInfo.parameters.filter((p) => p.location === "path");
484
- const queryParams = opInfo.parameters.filter((p) => p.location === "query");
485
504
  const hasPathParams = pathParams.length > 0;
486
- const hasQueryParams = queryParams.length > 0;
487
505
  const hasBody = !!opInfo.body;
488
- // Build extractor lines and request construction expression
506
+ // Build extractor lines and server method call arguments
507
+ // IMPORTANT: axum requires specific extractor order:
508
+ // State -> Extension -> Path -> Query -> Json -> Body
489
509
  const extractorLines = [];
490
- let requestExpr = "";
510
+ const serverArgs = [];
511
+ // State is always first (added in handler template)
512
+ // Extension (claims) comes after State
513
+ if (isProtected) {
514
+ extractorLines.push(` Extension(claims): Extension<S::Claims>,`);
515
+ serverArgs.push(`claims`);
516
+ }
517
+ // Path params come after Extension
491
518
  if (hasPathParams) {
492
519
  const pathTypes = pathParams.map((p) => p.rustType).join(", ");
493
520
  const pathFields = pathParams.map((p) => p.rustName).join(", ");
494
521
  if (pathParams.length === 1) {
495
- extractorLines.push(` axum::extract::Path(${pathFields}): axum::extract::Path<${pathTypes}>,`);
522
+ extractorLines.push(` Path(${pathFields}): Path<${pathTypes}>,`);
496
523
  }
497
524
  else {
498
- extractorLines.push(` axum::extract::Path((${pathFields})): axum::extract::Path<(${pathTypes})>,`);
525
+ extractorLines.push(` Path((${pathFields})): Path<(${pathTypes})>,`);
499
526
  }
500
- }
501
- if (hasQueryParams) {
502
- extractorLines.push(` axum::extract::Query(query): axum::extract::Query<${requestName}>,`);
503
- }
504
- else if (hasBody) {
505
- extractorLines.push(` axum::Json(body): axum::Json<${requestName}Body>,`);
506
- }
507
- if (isProtected) {
508
- extractorLines.push(` axum::Extension(claims): axum::Extension<S::Claims>,`);
509
- }
510
- // Build request struct expression
511
- if (hasOnlyPathParams(hasPathParams, hasQueryParams, hasBody)) {
512
- const pathAssignments = pathParams
513
- .map((p) => `${p.rustName},`)
514
- .join(" ");
515
- requestExpr = `${requestName} { ${pathAssignments} }`;
516
- }
517
- else if (hasQueryParams) {
518
- if (hasPathParams) {
519
- const pathAssignments = pathParams
520
- .map((p) => `${p.rustName},`)
521
- .join(" ");
522
- requestExpr = `${requestName} { ${pathAssignments} ..query }`;
523
- }
524
- else {
525
- requestExpr = "query";
527
+ // Add path params to server method args
528
+ for (const param of pathParams) {
529
+ serverArgs.push(param.rustName);
526
530
  }
527
531
  }
528
- else if (hasBody) {
529
- if (hasPathParams) {
530
- const pathAssignments = pathParams
531
- .map((p) => `${p.rustName},`)
532
- .join(" ");
533
- requestExpr = `${requestName} { ${pathAssignments} body }`;
534
- }
535
- else {
536
- requestExpr = `${requestName} { body }`;
537
- }
538
- }
539
- else {
540
- requestExpr = `${requestName} {}`;
532
+ // Json body comes last
533
+ if (hasBody && opInfo.body) {
534
+ const bodyType = getRustTypeForProperty(opInfo.body.type, program, anonymousEnums);
535
+ extractorLines.push(` Json(payload): Json<${bodyType.type}>,`);
536
+ serverArgs.push(`payload`);
541
537
  }
542
- // Server method call
543
- const serverCall = isProtected
544
- ? `service.${traitFnName}(claims, ${requestExpr}).await`
545
- : `service.${traitFnName}(${requestExpr}).await`;
538
+ // Build server method call
539
+ const serverArgsStr = serverArgs.join(", ");
540
+ const serverCall = `service.${traitFnName}(${serverArgsStr}).await`;
546
541
  // All handlers use <S> generics, Claims is now an associated type
547
542
  let handlerCode = `pub async fn ${handlerFnName}_handler<S>(
548
543
  axum::extract::State(service): axum::extract::State<S>,
@@ -1118,15 +1113,9 @@ export async function $onEmit(context, _options) {
1118
1113
  });
1119
1114
  if (namespaceGroups.length > 0) {
1120
1115
  const serverTrait = generateServerTrait(context.program, namespaceGroups, anonymousEnums);
1121
- const requestStructs = generateRequestStructs(context.program, namespaceGroups, anonymousEnums);
1122
1116
  const responseEnums = generateResponseEnums(context.program, namespaceGroups, anonymousEnums);
1123
1117
  const router = generateRouter(context.program, namespaceGroups, anonymousEnums);
1124
- const serverContent = [
1125
- serverTrait,
1126
- requestStructs,
1127
- responseEnums,
1128
- router,
1129
- ].join("\n");
1118
+ const serverContent = [serverTrait, responseEnums, router].join("\n");
1130
1119
  await emitFile(context.program, {
1131
1120
  path: resolvePath(context.emitterOutputDir, "server.rs"),
1132
1121
  content: serverContent,