typespec-rust-emitter 0.6.0 → 0.8.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/.qwen/settings.json +3 -2
- package/AGENTS.md +167 -0
- package/CHANGELOG.md +76 -0
- package/README.md +1 -1
- package/dist/src/emitter.js +43 -6
- package/dist/src/emitter.js.map +1 -1
- package/dist/src/testing/index.js +1 -1
- package/dist/src/testing/index.js.map +1 -1
- package/example/output-rust/src/generated/server.rs +71 -70
- package/example/output-rust/src/generated/types.rs +28 -0
- package/example/package-lock.json +1 -1
- package/package.json +1 -1
- package/src/emitter.ts +43 -6
- package/src/testing/index.ts +9 -5
- package/DEV.md +0 -81
- package/QWEN.md +0 -292
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
use super::types::*;
|
|
4
4
|
use async_trait::async_trait;
|
|
5
|
-
use axum::{http::StatusCode, Json};
|
|
6
5
|
use axum::extract::Path;
|
|
7
6
|
use axum::Extension;
|
|
7
|
+
use axum::http::StatusCode;
|
|
8
|
+
use axum::response::IntoResponse;
|
|
9
|
+
use axum::Json;
|
|
8
10
|
use eyre::Result;
|
|
9
11
|
|
|
10
12
|
#[async_trait]
|
|
@@ -51,7 +53,7 @@ pub trait Server: Send + Sync {
|
|
|
51
53
|
}
|
|
52
54
|
pub enum GroupsListResponse {
|
|
53
55
|
Ok(Json<Vec<GroupStatistics>>),
|
|
54
|
-
Unauthorized,
|
|
56
|
+
Unauthorized(Json<ApiError>),
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
impl IntoResponse for GroupsListResponse {
|
|
@@ -59,15 +61,15 @@ impl IntoResponse for GroupsListResponse {
|
|
|
59
61
|
match self {
|
|
60
62
|
|
|
61
63
|
GroupsListResponse::Ok(body) => (StatusCode::OK, body).into_response(),
|
|
62
|
-
GroupsListResponse::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
|
64
|
+
GroupsListResponse::Unauthorized(body) => (StatusCode::UNAUTHORIZED, body).into_response(),
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
pub enum GroupsCreateResponse {
|
|
68
70
|
Created(Json<Group>),
|
|
69
|
-
BadRequest,
|
|
70
|
-
Unauthorized,
|
|
71
|
+
BadRequest(Json<ValidationError>),
|
|
72
|
+
Unauthorized(Json<ApiError>),
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
impl IntoResponse for GroupsCreateResponse {
|
|
@@ -75,16 +77,16 @@ impl IntoResponse for GroupsCreateResponse {
|
|
|
75
77
|
match self {
|
|
76
78
|
|
|
77
79
|
GroupsCreateResponse::Created(body) => (StatusCode::CREATED, body).into_response(),
|
|
78
|
-
GroupsCreateResponse::BadRequest => StatusCode::BAD_REQUEST.into_response(),
|
|
79
|
-
GroupsCreateResponse::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
|
80
|
+
GroupsCreateResponse::BadRequest(body) => (StatusCode::BAD_REQUEST, body).into_response(),
|
|
81
|
+
GroupsCreateResponse::Unauthorized(body) => (StatusCode::UNAUTHORIZED, body).into_response(),
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
pub enum GroupsGetByIdResponse {
|
|
85
87
|
Ok(Json<GroupStatistics>),
|
|
86
|
-
Unauthorized,
|
|
87
|
-
NotFound,
|
|
88
|
+
Unauthorized(Json<ApiError>),
|
|
89
|
+
NotFound(Json<NotFoundError>),
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
impl IntoResponse for GroupsGetByIdResponse {
|
|
@@ -92,17 +94,17 @@ impl IntoResponse for GroupsGetByIdResponse {
|
|
|
92
94
|
match self {
|
|
93
95
|
|
|
94
96
|
GroupsGetByIdResponse::Ok(body) => (StatusCode::OK, body).into_response(),
|
|
95
|
-
GroupsGetByIdResponse::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
|
96
|
-
GroupsGetByIdResponse::NotFound => StatusCode::NOT_FOUND.into_response(),
|
|
97
|
+
GroupsGetByIdResponse::Unauthorized(body) => (StatusCode::UNAUTHORIZED, body).into_response(),
|
|
98
|
+
GroupsGetByIdResponse::NotFound(body) => (StatusCode::NOT_FOUND, body).into_response(),
|
|
97
99
|
}
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
pub enum GroupsUpdateResponse {
|
|
102
104
|
Ok(Json<Group>),
|
|
103
|
-
BadRequest,
|
|
104
|
-
Unauthorized,
|
|
105
|
-
NotFound,
|
|
105
|
+
BadRequest(Json<ValidationError>),
|
|
106
|
+
Unauthorized(Json<ApiError>),
|
|
107
|
+
NotFound(Json<NotFoundError>),
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
impl IntoResponse for GroupsUpdateResponse {
|
|
@@ -110,17 +112,17 @@ impl IntoResponse for GroupsUpdateResponse {
|
|
|
110
112
|
match self {
|
|
111
113
|
|
|
112
114
|
GroupsUpdateResponse::Ok(body) => (StatusCode::OK, body).into_response(),
|
|
113
|
-
GroupsUpdateResponse::BadRequest => StatusCode::BAD_REQUEST.into_response(),
|
|
114
|
-
GroupsUpdateResponse::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
|
115
|
-
GroupsUpdateResponse::NotFound => StatusCode::NOT_FOUND.into_response(),
|
|
115
|
+
GroupsUpdateResponse::BadRequest(body) => (StatusCode::BAD_REQUEST, body).into_response(),
|
|
116
|
+
GroupsUpdateResponse::Unauthorized(body) => (StatusCode::UNAUTHORIZED, body).into_response(),
|
|
117
|
+
GroupsUpdateResponse::NotFound(body) => (StatusCode::NOT_FOUND, body).into_response(),
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
pub enum GroupsDeleteResponse {
|
|
121
123
|
NoContent,
|
|
122
|
-
Unauthorized,
|
|
123
|
-
NotFound,
|
|
124
|
+
Unauthorized(Json<ApiError>),
|
|
125
|
+
NotFound(Json<NotFoundError>),
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
impl IntoResponse for GroupsDeleteResponse {
|
|
@@ -128,16 +130,16 @@ impl IntoResponse for GroupsDeleteResponse {
|
|
|
128
130
|
match self {
|
|
129
131
|
|
|
130
132
|
GroupsDeleteResponse::NoContent => StatusCode::NO_CONTENT.into_response(),
|
|
131
|
-
GroupsDeleteResponse::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
|
132
|
-
GroupsDeleteResponse::NotFound => StatusCode::NOT_FOUND.into_response(),
|
|
133
|
+
GroupsDeleteResponse::Unauthorized(body) => (StatusCode::UNAUTHORIZED, body).into_response(),
|
|
134
|
+
GroupsDeleteResponse::NotFound(body) => (StatusCode::NOT_FOUND, body).into_response(),
|
|
133
135
|
}
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
pub enum SubjectsListResponse {
|
|
138
140
|
Ok(Json<Vec<SubjectStatistics>>),
|
|
139
|
-
Unauthorized,
|
|
140
|
-
NotFound,
|
|
141
|
+
Unauthorized(Json<ApiError>),
|
|
142
|
+
NotFound(Json<NotFoundError>),
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
impl IntoResponse for SubjectsListResponse {
|
|
@@ -145,17 +147,17 @@ impl IntoResponse for SubjectsListResponse {
|
|
|
145
147
|
match self {
|
|
146
148
|
|
|
147
149
|
SubjectsListResponse::Ok(body) => (StatusCode::OK, body).into_response(),
|
|
148
|
-
SubjectsListResponse::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
|
149
|
-
SubjectsListResponse::NotFound => StatusCode::NOT_FOUND.into_response(),
|
|
150
|
+
SubjectsListResponse::Unauthorized(body) => (StatusCode::UNAUTHORIZED, body).into_response(),
|
|
151
|
+
SubjectsListResponse::NotFound(body) => (StatusCode::NOT_FOUND, body).into_response(),
|
|
150
152
|
}
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
pub enum SubjectsCreateResponse {
|
|
155
157
|
Created(Json<Subject>),
|
|
156
|
-
BadRequest,
|
|
157
|
-
Unauthorized,
|
|
158
|
-
NotFound,
|
|
158
|
+
BadRequest(Json<ValidationError>),
|
|
159
|
+
Unauthorized(Json<ApiError>),
|
|
160
|
+
NotFound(Json<NotFoundError>),
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
impl IntoResponse for SubjectsCreateResponse {
|
|
@@ -163,17 +165,17 @@ impl IntoResponse for SubjectsCreateResponse {
|
|
|
163
165
|
match self {
|
|
164
166
|
|
|
165
167
|
SubjectsCreateResponse::Created(body) => (StatusCode::CREATED, body).into_response(),
|
|
166
|
-
SubjectsCreateResponse::BadRequest => StatusCode::BAD_REQUEST.into_response(),
|
|
167
|
-
SubjectsCreateResponse::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
|
168
|
-
SubjectsCreateResponse::NotFound => StatusCode::NOT_FOUND.into_response(),
|
|
168
|
+
SubjectsCreateResponse::BadRequest(body) => (StatusCode::BAD_REQUEST, body).into_response(),
|
|
169
|
+
SubjectsCreateResponse::Unauthorized(body) => (StatusCode::UNAUTHORIZED, body).into_response(),
|
|
170
|
+
SubjectsCreateResponse::NotFound(body) => (StatusCode::NOT_FOUND, body).into_response(),
|
|
169
171
|
}
|
|
170
172
|
}
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
pub enum SubjectsGetByIdResponse {
|
|
174
176
|
Ok(Json<SubjectStatistics>),
|
|
175
|
-
Unauthorized,
|
|
176
|
-
NotFound,
|
|
177
|
+
Unauthorized(Json<ApiError>),
|
|
178
|
+
NotFound(Json<NotFoundError>),
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
impl IntoResponse for SubjectsGetByIdResponse {
|
|
@@ -181,17 +183,17 @@ impl IntoResponse for SubjectsGetByIdResponse {
|
|
|
181
183
|
match self {
|
|
182
184
|
|
|
183
185
|
SubjectsGetByIdResponse::Ok(body) => (StatusCode::OK, body).into_response(),
|
|
184
|
-
SubjectsGetByIdResponse::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
|
185
|
-
SubjectsGetByIdResponse::NotFound => StatusCode::NOT_FOUND.into_response(),
|
|
186
|
+
SubjectsGetByIdResponse::Unauthorized(body) => (StatusCode::UNAUTHORIZED, body).into_response(),
|
|
187
|
+
SubjectsGetByIdResponse::NotFound(body) => (StatusCode::NOT_FOUND, body).into_response(),
|
|
186
188
|
}
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
pub enum SubjectsUpdateResponse {
|
|
191
193
|
Ok(Json<Subject>),
|
|
192
|
-
BadRequest,
|
|
193
|
-
Unauthorized,
|
|
194
|
-
NotFound,
|
|
194
|
+
BadRequest(Json<ValidationError>),
|
|
195
|
+
Unauthorized(Json<ApiError>),
|
|
196
|
+
NotFound(Json<NotFoundError>),
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
impl IntoResponse for SubjectsUpdateResponse {
|
|
@@ -199,17 +201,17 @@ impl IntoResponse for SubjectsUpdateResponse {
|
|
|
199
201
|
match self {
|
|
200
202
|
|
|
201
203
|
SubjectsUpdateResponse::Ok(body) => (StatusCode::OK, body).into_response(),
|
|
202
|
-
SubjectsUpdateResponse::BadRequest => StatusCode::BAD_REQUEST.into_response(),
|
|
203
|
-
SubjectsUpdateResponse::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
|
204
|
-
SubjectsUpdateResponse::NotFound => StatusCode::NOT_FOUND.into_response(),
|
|
204
|
+
SubjectsUpdateResponse::BadRequest(body) => (StatusCode::BAD_REQUEST, body).into_response(),
|
|
205
|
+
SubjectsUpdateResponse::Unauthorized(body) => (StatusCode::UNAUTHORIZED, body).into_response(),
|
|
206
|
+
SubjectsUpdateResponse::NotFound(body) => (StatusCode::NOT_FOUND, body).into_response(),
|
|
205
207
|
}
|
|
206
208
|
}
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
pub enum SubjectsDeleteResponse {
|
|
210
212
|
NoContent,
|
|
211
|
-
Unauthorized,
|
|
212
|
-
NotFound,
|
|
213
|
+
Unauthorized(Json<ApiError>),
|
|
214
|
+
NotFound(Json<NotFoundError>),
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
impl IntoResponse for SubjectsDeleteResponse {
|
|
@@ -217,17 +219,17 @@ impl IntoResponse for SubjectsDeleteResponse {
|
|
|
217
219
|
match self {
|
|
218
220
|
|
|
219
221
|
SubjectsDeleteResponse::NoContent => StatusCode::NO_CONTENT.into_response(),
|
|
220
|
-
SubjectsDeleteResponse::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
|
221
|
-
SubjectsDeleteResponse::NotFound => StatusCode::NOT_FOUND.into_response(),
|
|
222
|
+
SubjectsDeleteResponse::Unauthorized(body) => (StatusCode::UNAUTHORIZED, body).into_response(),
|
|
223
|
+
SubjectsDeleteResponse::NotFound(body) => (StatusCode::NOT_FOUND, body).into_response(),
|
|
222
224
|
}
|
|
223
225
|
}
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
pub enum SessionsStartResponse {
|
|
227
229
|
Created(Json<SessionOpenedResponse>),
|
|
228
|
-
Unauthorized,
|
|
229
|
-
NotFound,
|
|
230
|
-
Conflict,
|
|
230
|
+
Unauthorized(Json<ApiError>),
|
|
231
|
+
NotFound(Json<NotFoundError>),
|
|
232
|
+
Conflict(Json<ConflictError>),
|
|
231
233
|
}
|
|
232
234
|
|
|
233
235
|
impl IntoResponse for SessionsStartResponse {
|
|
@@ -235,18 +237,18 @@ impl IntoResponse for SessionsStartResponse {
|
|
|
235
237
|
match self {
|
|
236
238
|
|
|
237
239
|
SessionsStartResponse::Created(body) => (StatusCode::CREATED, body).into_response(),
|
|
238
|
-
SessionsStartResponse::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
|
239
|
-
SessionsStartResponse::NotFound => StatusCode::NOT_FOUND.into_response(),
|
|
240
|
-
SessionsStartResponse::Conflict => StatusCode::CONFLICT.into_response(),
|
|
240
|
+
SessionsStartResponse::Unauthorized(body) => (StatusCode::UNAUTHORIZED, body).into_response(),
|
|
241
|
+
SessionsStartResponse::NotFound(body) => (StatusCode::NOT_FOUND, body).into_response(),
|
|
242
|
+
SessionsStartResponse::Conflict(body) => (StatusCode::CONFLICT, body).into_response(),
|
|
241
243
|
}
|
|
242
244
|
}
|
|
243
245
|
}
|
|
244
246
|
|
|
245
247
|
pub enum SessionsPauseResponse {
|
|
246
248
|
Ok(Json<SessionClosedResponse>),
|
|
247
|
-
Unauthorized,
|
|
248
|
-
NotFound,
|
|
249
|
-
Conflict,
|
|
249
|
+
Unauthorized(Json<ApiError>),
|
|
250
|
+
NotFound(Json<NotFoundError>),
|
|
251
|
+
Conflict(Json<ConflictError>),
|
|
250
252
|
}
|
|
251
253
|
|
|
252
254
|
impl IntoResponse for SessionsPauseResponse {
|
|
@@ -254,18 +256,18 @@ impl IntoResponse for SessionsPauseResponse {
|
|
|
254
256
|
match self {
|
|
255
257
|
|
|
256
258
|
SessionsPauseResponse::Ok(body) => (StatusCode::OK, body).into_response(),
|
|
257
|
-
SessionsPauseResponse::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
|
258
|
-
SessionsPauseResponse::NotFound => StatusCode::NOT_FOUND.into_response(),
|
|
259
|
-
SessionsPauseResponse::Conflict => StatusCode::CONFLICT.into_response(),
|
|
259
|
+
SessionsPauseResponse::Unauthorized(body) => (StatusCode::UNAUTHORIZED, body).into_response(),
|
|
260
|
+
SessionsPauseResponse::NotFound(body) => (StatusCode::NOT_FOUND, body).into_response(),
|
|
261
|
+
SessionsPauseResponse::Conflict(body) => (StatusCode::CONFLICT, body).into_response(),
|
|
260
262
|
}
|
|
261
263
|
}
|
|
262
264
|
}
|
|
263
265
|
|
|
264
266
|
pub enum SessionsResumeResponse {
|
|
265
267
|
Ok(Json<SessionOpenedResponse>),
|
|
266
|
-
Unauthorized,
|
|
267
|
-
NotFound,
|
|
268
|
-
Conflict,
|
|
268
|
+
Unauthorized(Json<ApiError>),
|
|
269
|
+
NotFound(Json<NotFoundError>),
|
|
270
|
+
Conflict(Json<ConflictError>),
|
|
269
271
|
}
|
|
270
272
|
|
|
271
273
|
impl IntoResponse for SessionsResumeResponse {
|
|
@@ -273,18 +275,18 @@ impl IntoResponse for SessionsResumeResponse {
|
|
|
273
275
|
match self {
|
|
274
276
|
|
|
275
277
|
SessionsResumeResponse::Ok(body) => (StatusCode::OK, body).into_response(),
|
|
276
|
-
SessionsResumeResponse::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
|
277
|
-
SessionsResumeResponse::NotFound => StatusCode::NOT_FOUND.into_response(),
|
|
278
|
-
SessionsResumeResponse::Conflict => StatusCode::CONFLICT.into_response(),
|
|
278
|
+
SessionsResumeResponse::Unauthorized(body) => (StatusCode::UNAUTHORIZED, body).into_response(),
|
|
279
|
+
SessionsResumeResponse::NotFound(body) => (StatusCode::NOT_FOUND, body).into_response(),
|
|
280
|
+
SessionsResumeResponse::Conflict(body) => (StatusCode::CONFLICT, body).into_response(),
|
|
279
281
|
}
|
|
280
282
|
}
|
|
281
283
|
}
|
|
282
284
|
|
|
283
285
|
pub enum SessionsStopResponse {
|
|
284
286
|
Ok(Json<SessionClosedResponse>),
|
|
285
|
-
Unauthorized,
|
|
286
|
-
NotFound,
|
|
287
|
-
Conflict,
|
|
287
|
+
Unauthorized(Json<ApiError>),
|
|
288
|
+
NotFound(Json<NotFoundError>),
|
|
289
|
+
Conflict(Json<ConflictError>),
|
|
288
290
|
}
|
|
289
291
|
|
|
290
292
|
impl IntoResponse for SessionsStopResponse {
|
|
@@ -292,14 +294,13 @@ impl IntoResponse for SessionsStopResponse {
|
|
|
292
294
|
match self {
|
|
293
295
|
|
|
294
296
|
SessionsStopResponse::Ok(body) => (StatusCode::OK, body).into_response(),
|
|
295
|
-
SessionsStopResponse::Unauthorized => StatusCode::UNAUTHORIZED.into_response(),
|
|
296
|
-
SessionsStopResponse::NotFound => StatusCode::NOT_FOUND.into_response(),
|
|
297
|
-
SessionsStopResponse::Conflict => StatusCode::CONFLICT.into_response(),
|
|
297
|
+
SessionsStopResponse::Unauthorized(body) => (StatusCode::UNAUTHORIZED, body).into_response(),
|
|
298
|
+
SessionsStopResponse::NotFound(body) => (StatusCode::NOT_FOUND, body).into_response(),
|
|
299
|
+
SessionsStopResponse::Conflict(body) => (StatusCode::CONFLICT, body).into_response(),
|
|
298
300
|
}
|
|
299
301
|
}
|
|
300
302
|
}
|
|
301
303
|
|
|
302
|
-
use axum::response::IntoResponse;
|
|
303
304
|
use axum::routing::{delete, get, patch, post};
|
|
304
305
|
use axum::Router;
|
|
305
306
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#![allow(unused)]
|
|
2
2
|
|
|
3
|
+
use axum::http::StatusCode;
|
|
4
|
+
use axum::response::IntoResponse;
|
|
5
|
+
use axum::Json;
|
|
6
|
+
|
|
3
7
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, thiserror::Error)]
|
|
4
8
|
#[error("{code}: {message}")]
|
|
5
9
|
pub struct ApiError {
|
|
@@ -9,6 +13,12 @@ pub struct ApiError {
|
|
|
9
13
|
pub message: String,
|
|
10
14
|
}
|
|
11
15
|
|
|
16
|
+
impl IntoResponse for ApiError {
|
|
17
|
+
fn into_response(self) -> axum::response::Response {
|
|
18
|
+
(StatusCode::INTERNAL_SERVER_ERROR, Json(self)).into_response()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, thiserror::Error)]
|
|
13
23
|
#[error("{code}: {message}")]
|
|
14
24
|
pub struct NotFoundError {
|
|
@@ -17,6 +27,12 @@ pub struct NotFoundError {
|
|
|
17
27
|
pub message: String,
|
|
18
28
|
}
|
|
19
29
|
|
|
30
|
+
impl IntoResponse for NotFoundError {
|
|
31
|
+
fn into_response(self) -> axum::response::Response {
|
|
32
|
+
(StatusCode::NOT_FOUND, Json(self)).into_response()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, thiserror::Error)]
|
|
21
37
|
#[error("{code}: {message}")]
|
|
22
38
|
pub struct ValidationError {
|
|
@@ -25,6 +41,12 @@ pub struct ValidationError {
|
|
|
25
41
|
pub message: String,
|
|
26
42
|
}
|
|
27
43
|
|
|
44
|
+
impl IntoResponse for ValidationError {
|
|
45
|
+
fn into_response(self) -> axum::response::Response {
|
|
46
|
+
(StatusCode::BAD_REQUEST, Json(self)).into_response()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, thiserror::Error)]
|
|
29
51
|
#[error("{code}: {message}")]
|
|
30
52
|
pub struct ConflictError {
|
|
@@ -33,6 +55,12 @@ pub struct ConflictError {
|
|
|
33
55
|
pub message: String,
|
|
34
56
|
}
|
|
35
57
|
|
|
58
|
+
impl IntoResponse for ConflictError {
|
|
59
|
+
fn into_response(self) -> axum::response::Response {
|
|
60
|
+
(StatusCode::CONFLICT, Json(self)).into_response()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
36
64
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
37
65
|
pub struct DurationStats {
|
|
38
66
|
#[serde(rename = "durationTotal")]
|
package/package.json
CHANGED
package/src/emitter.ts
CHANGED
|
@@ -417,8 +417,21 @@ function getBodyFromResponse(
|
|
|
417
417
|
): { type: string | undefined; description: string | undefined } {
|
|
418
418
|
if (variant.type.kind === "Model") {
|
|
419
419
|
const model = variant.type as Model;
|
|
420
|
-
for (const [
|
|
421
|
-
|
|
420
|
+
for (const [_propName, prop] of model.properties) {
|
|
421
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
422
|
+
const decorators = (prop as any).decorators;
|
|
423
|
+
let isBody = false;
|
|
424
|
+
if (decorators) {
|
|
425
|
+
for (const key of Object.keys(decorators)) {
|
|
426
|
+
const decorator = decorators[key];
|
|
427
|
+
const name = getDecoratorName(decorator);
|
|
428
|
+
if (name === "body" || name === "bodyRoot") {
|
|
429
|
+
isBody = true;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (isBody) {
|
|
422
435
|
const { type: rustType } = getRustTypeForProperty(
|
|
423
436
|
prop.type,
|
|
424
437
|
program,
|
|
@@ -545,6 +558,13 @@ function getHttpStatusCode(statusCode: number): string {
|
|
|
545
558
|
return statusCodes[statusCode] || `StatusCode::from_u16(${statusCode})`;
|
|
546
559
|
}
|
|
547
560
|
|
|
561
|
+
function getHttpStatusCodeForError(errorName: string): string {
|
|
562
|
+
if (errorName.endsWith("NotFoundError")) return "NOT_FOUND";
|
|
563
|
+
if (errorName.endsWith("ValidationError")) return "BAD_REQUEST";
|
|
564
|
+
if (errorName.endsWith("ConflictError")) return "CONFLICT";
|
|
565
|
+
return "INTERNAL_SERVER_ERROR";
|
|
566
|
+
}
|
|
567
|
+
|
|
548
568
|
function generateServerTrait(
|
|
549
569
|
program: Program,
|
|
550
570
|
namespaceGroups: { namespace: Namespace; operations: Operation[] }[],
|
|
@@ -554,9 +574,11 @@ function generateServerTrait(
|
|
|
554
574
|
|
|
555
575
|
parts.push(`use super::types::*;
|
|
556
576
|
use async_trait::async_trait;
|
|
557
|
-
use axum::{http::StatusCode, Json};
|
|
558
577
|
use axum::extract::Path;
|
|
559
578
|
use axum::Extension;
|
|
579
|
+
use axum::http::StatusCode;
|
|
580
|
+
use axum::response::IntoResponse;
|
|
581
|
+
use axum::Json;
|
|
560
582
|
use eyre::Result;
|
|
561
583
|
|
|
562
584
|
#[async_trait]
|
|
@@ -812,8 +834,7 @@ where
|
|
|
812
834
|
const routerBody = buildRouterBody(publicRoutes, protectedRoutes);
|
|
813
835
|
|
|
814
836
|
const parts: string[] = [];
|
|
815
|
-
parts.push(`use axum::
|
|
816
|
-
use axum::routing::{${methodImports}};
|
|
837
|
+
parts.push(`use axum::routing::{${methodImports}};
|
|
817
838
|
use axum::Router;
|
|
818
839
|
|
|
819
840
|
`);
|
|
@@ -1232,6 +1253,16 @@ ${fields.join("\n")}
|
|
|
1232
1253
|
parts.push("(());");
|
|
1233
1254
|
}
|
|
1234
1255
|
|
|
1256
|
+
if (isError && allProps.size > 0) {
|
|
1257
|
+
const statusCode = getHttpStatusCodeForError(name);
|
|
1258
|
+
parts.push(`
|
|
1259
|
+
impl IntoResponse for ${name} {
|
|
1260
|
+
fn into_response(self) -> axum::response::Response {
|
|
1261
|
+
(StatusCode::${statusCode}, Json(self)).into_response()
|
|
1262
|
+
}
|
|
1263
|
+
}`);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1235
1266
|
return parts.join("\n");
|
|
1236
1267
|
}
|
|
1237
1268
|
|
|
@@ -1508,7 +1539,13 @@ export async function $onEmit(
|
|
|
1508
1539
|
|
|
1509
1540
|
const namespaceGroups = getAllOperations(context.program);
|
|
1510
1541
|
|
|
1511
|
-
const outputContent =
|
|
1542
|
+
const outputContent =
|
|
1543
|
+
"#![allow(unused)]\n\n" +
|
|
1544
|
+
"use axum::http::StatusCode;\n" +
|
|
1545
|
+
"use axum::response::IntoResponse;\n" +
|
|
1546
|
+
"use axum::Json;\n\n" +
|
|
1547
|
+
content.join("\n") +
|
|
1548
|
+
"\n";
|
|
1512
1549
|
|
|
1513
1550
|
await emitFile(context.program, {
|
|
1514
1551
|
path: resolvePath(context.emitterOutputDir, "types.rs"),
|
package/src/testing/index.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { resolvePath } from "@typespec/compiler";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createTestLibrary,
|
|
4
|
+
TypeSpecTestLibrary,
|
|
5
|
+
} from "@typespec/compiler/testing";
|
|
3
6
|
import { fileURLToPath } from "url";
|
|
4
7
|
|
|
5
|
-
export const TypespecEmitterTestLibrary: TypeSpecTestLibrary =
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
export const TypespecEmitterTestLibrary: TypeSpecTestLibrary =
|
|
9
|
+
createTestLibrary({
|
|
10
|
+
name: "typespec-emitter",
|
|
11
|
+
packageRoot: resolvePath(fileURLToPath(import.meta.url), "../../../../"),
|
|
12
|
+
});
|
package/DEV.md
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# TypeSpec Rust Emitter - LLM Context
|
|
2
|
-
|
|
3
|
-
## Purpose
|
|
4
|
-
|
|
5
|
-
TypeSpec emitter that generates idiomatic Rust code (structs, enums, server traits) from TypeSpec specifications.
|
|
6
|
-
|
|
7
|
-
## Key Paths
|
|
8
|
-
|
|
9
|
-
| Path | Purpose |
|
|
10
|
-
| ------------------------------------ | ------------------------------------------------------- |
|
|
11
|
-
| `src/emitter.ts` | Main emitter logic (1464 lines) - TypeSpec→Rust codegen |
|
|
12
|
-
| `src/lib.tsp` | Decorator declarations (`@rustDerive`, `@rustDerives`) |
|
|
13
|
-
| `test/hello.test.ts` | Unit tests using `emit()` helper |
|
|
14
|
-
| `example/lib/learning/` | Demo TypeSpec models & operations |
|
|
15
|
-
| `example/output-rust/src/generated/` | Generated Rust output |
|
|
16
|
-
| `oas3-gen/crates/oas3-gen/` | Reference template for codegen patterns |
|
|
17
|
-
|
|
18
|
-
## Commands (Run After Every Change)
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
just build # npm run build - Compile TypeScript
|
|
22
|
-
just test # npm test - Run unit tests
|
|
23
|
-
just compile # tsp compile example → generates Rust code
|
|
24
|
-
just check-rust # cargo check - Verify Rust compiles
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Type Mappings
|
|
28
|
-
|
|
29
|
-
| TypeSpec | Rust |
|
|
30
|
-
| ---------------------- | ----------------------- |
|
|
31
|
-
| `string` | `String` |
|
|
32
|
-
| `int32/64` | `i32/i64` |
|
|
33
|
-
| `T \| null` | `Option<T>` |
|
|
34
|
-
| `T[]` | `Vec<T>` |
|
|
35
|
-
| `Record<T>` | `HashMap<String, T>` |
|
|
36
|
-
| `@format("uuid")` | `uuid::Uuid` |
|
|
37
|
-
| `@format("date-time")` | `chrono::DateTime<Utc>` |
|
|
38
|
-
|
|
39
|
-
## Architecture
|
|
40
|
-
|
|
41
|
-
1. `navigateProgram()` walks TypeSpec AST
|
|
42
|
-
2. Collects models, enums, operations
|
|
43
|
-
3. Processes decorators
|
|
44
|
-
4. Generates Rust code via string templates
|
|
45
|
-
5. Emits to `example/output-rust/src/generated/`
|
|
46
|
-
|
|
47
|
-
## Server Trait Generation
|
|
48
|
-
|
|
49
|
-
- Generates `Server<Claims>` trait with async methods per operation
|
|
50
|
-
- Handler functions: `pub async fn {op}_handler<S, Claims>(...)`
|
|
51
|
-
- All handlers use `<S, Claims>` generics (even public routes)
|
|
52
|
-
- Protected routes (`@useAuth`) receive `claims: Claims` parameter
|
|
53
|
-
- Router splits public/protected routes, merges at end
|
|
54
|
-
|
|
55
|
-
## Test Pattern
|
|
56
|
-
|
|
57
|
-
```typescript
|
|
58
|
-
import { emit } from "./test-host.js";
|
|
59
|
-
const results = await emit(`model User { name: string; }`);
|
|
60
|
-
const output = results["types.rs"];
|
|
61
|
-
strictEqual(output.includes("pub struct User"), true);
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## Recent Fix (2026-03-27)
|
|
65
|
-
|
|
66
|
-
All handler functions now use `<S, Claims>` generics uniformly. Public routes include `Claims: Send + Sync + Clone + 'static` bound even without using Claims, because Server trait requires it.
|
|
67
|
-
|
|
68
|
-
## Rules
|
|
69
|
-
|
|
70
|
-
1. Always run full cycle: `build → test → compile → check-rust`
|
|
71
|
-
2. TypeScript strict mode enabled - no implicit any
|
|
72
|
-
3. Match existing code patterns in `src/emitter.ts`
|
|
73
|
-
4. Add tests for new features
|
|
74
|
-
5. Generated Rust must compile
|
|
75
|
-
|
|
76
|
-
## Todo
|
|
77
|
-
|
|
78
|
-
- [ ] More server trait test coverage
|
|
79
|
-
- [ ] Integration tests with Rust compilation
|
|
80
|
-
- [ ] Expand scalar format mappings
|
|
81
|
-
- [ ] Full working Rust server example
|