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 +56 -0
- package/QWEN.md +83 -29
- package/README.md +27 -20
- package/dist/src/emitter.js +97 -108
- package/dist/src/emitter.js.map +1 -1
- package/example/output-rust/src/generated/server.rs +58 -156
- package/example/package-lock.json +1 -1
- package/package.json +1 -1
- package/src/emitter.ts +112 -140
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
|
|
65
|
-
|
|
66
|
-
| `string`
|
|
67
|
-
| `int8/16/32/64`
|
|
68
|
-
| `uint8/16/32/64` | `u8/u16/u32/u64`
|
|
69
|
-
| `float32/64`
|
|
70
|
-
| `boolean`
|
|
71
|
-
| `bytes`
|
|
72
|
-
| `T[]`
|
|
73
|
-
| `Record<T>`
|
|
74
|
-
| `T \| null`
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
|
80
|
-
| `"
|
|
81
|
-
| `"
|
|
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
|
|
87
|
-
|
|
88
|
-
| `@error`
|
|
89
|
-
| `@pattern("regex")`
|
|
90
|
-
| `@rustDerive("...")`
|
|
91
|
-
| `@rustDerives("...", "...")` | Adds multiple custom derives (models & enums)
|
|
92
|
-
| `@rustAttr("...")`
|
|
93
|
-
| `@rustAttrs("...", "...")`
|
|
94
|
-
| `@doc("...")`
|
|
95
|
-
| `@useAuth(BearerAuth)`
|
|
96
|
-
| `@maxLength(n)`
|
|
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
|
|
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
|
-
-
|
|
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
|
|
252
|
-
async fn groups_create(&self, claims: Self::Claims,
|
|
253
|
-
async fn groups_get_by_id(&self,
|
|
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
|
-
|
|
283
|
+
body: CreateGroupBody,
|
|
287
284
|
) -> eyre::Result<GroupsCreateResponse> {
|
|
288
|
-
//
|
|
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(&
|
|
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
|
|
package/dist/src/emitter.js
CHANGED
|
@@ -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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(`
|
|
522
|
+
extractorLines.push(` Path(${pathFields}): Path<${pathTypes}>,`);
|
|
496
523
|
}
|
|
497
524
|
else {
|
|
498
|
-
extractorLines.push(`
|
|
525
|
+
extractorLines.push(` Path((${pathFields})): Path<(${pathTypes})>,`);
|
|
499
526
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
//
|
|
543
|
-
const
|
|
544
|
-
|
|
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,
|