typespec-rust-emitter 0.10.5 → 0.10.7

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.
@@ -6,366 +6,13 @@ use axum::response::IntoResponse;
6
6
  use axum::Json;
7
7
 
8
8
  #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
9
- pub struct ApiError {
10
- /// Machine-readable error code.
11
- pub code: String,
12
- /// Human-readable message.
9
+ pub struct MyEventData {
13
10
  pub message: String,
14
11
  }
15
12
 
16
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
17
- pub struct NotFoundError {
18
- pub code: String,
19
- /// Human-readable message.
20
- pub message: String,
21
- }
22
-
23
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
24
- pub struct ValidationError {
25
- pub code: String,
26
- /// Human-readable message.
27
- pub message: String,
28
- }
29
-
30
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
31
- pub struct ConflictError {
32
- pub code: String,
33
- /// Human-readable message.
34
- pub message: String,
35
- }
36
-
37
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
38
- pub struct DurationStats {
39
- #[serde(rename = "durationTotal")]
40
- pub duration_total: i64,
41
- #[serde(rename = "durationDay")]
42
- pub duration_day: i64,
43
- #[serde(rename = "durationWeek")]
44
- pub duration_week: i64,
45
- #[serde(rename = "durationMonth")]
46
- pub duration_month: i64,
47
- }
48
-
49
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
50
- pub struct CalendarHeatmapItem {
51
- pub date: String,
52
- pub duration: i64,
53
- }
54
-
55
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
56
- pub struct Group {
57
- pub id: i64,
58
- #[serde(rename = "accountId")]
59
- pub account_id: uuid::Uuid,
60
- #[serde(rename = "createdAt")]
61
- pub created_at: chrono::DateTime<chrono::Utc>,
62
- #[serde(rename = "deletedAt")]
63
- pub deleted_at: Option<chrono::DateTime<chrono::Utc>>,
64
- pub name: String,
65
- pub icon: String,
66
- #[serde(rename = "colorText")]
67
- pub color_text: HexColor,
68
- #[serde(rename = "colorBg")]
69
- pub color_bg: HexColor,
70
- }
71
-
72
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
73
- pub struct GroupStatistics {
74
- pub id: i64,
75
- #[serde(rename = "accountId")]
76
- pub account_id: uuid::Uuid,
77
- #[serde(rename = "createdAt")]
78
- pub created_at: chrono::DateTime<chrono::Utc>,
79
- #[serde(rename = "deletedAt")]
80
- pub deleted_at: Option<chrono::DateTime<chrono::Utc>>,
81
- pub name: String,
82
- pub icon: String,
83
- #[serde(rename = "colorText")]
84
- pub color_text: HexColor,
85
- #[serde(rename = "colorBg")]
86
- pub color_bg: HexColor,
87
- #[serde(rename = "subjectCount")]
88
- pub subject_count: i64,
89
- pub status: StudyStatus,
90
- #[serde(rename = "durationTotal")]
91
- pub duration_total: i64,
92
- #[serde(rename = "durationDay")]
93
- pub duration_day: i64,
94
- #[serde(rename = "durationWeek")]
95
- pub duration_week: i64,
96
- #[serde(rename = "durationMonth")]
97
- pub duration_month: i64,
98
- }
99
-
100
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
101
- pub struct CreateGroupBody {
102
- pub name: String,
103
- pub icon: String,
104
- #[serde(rename = "colorText")]
105
- pub color_text: HexColor,
106
- #[serde(rename = "colorBg")]
107
- pub color_bg: HexColor,
108
- }
109
-
110
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
111
- pub struct UpdateGroupBody {
112
- #[serde(skip_serializing_if = "Option::is_none")]
113
- pub name: Option<String>,
114
- #[serde(skip_serializing_if = "Option::is_none")]
115
- pub icon: Option<String>,
116
- #[serde(rename = "colorText")]
117
- #[serde(skip_serializing_if = "Option::is_none")]
118
- pub color_text: Option<HexColor>,
119
- #[serde(rename = "colorBg")]
120
- #[serde(skip_serializing_if = "Option::is_none")]
121
- pub color_bg: Option<HexColor>,
122
- }
123
-
124
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
125
- pub struct Subject {
126
- pub id: i64,
127
- #[serde(rename = "accountId")]
128
- pub account_id: uuid::Uuid,
129
- #[serde(rename = "groupId")]
130
- pub group_id: i64,
131
- #[serde(rename = "createdAt")]
132
- pub created_at: chrono::DateTime<chrono::Utc>,
133
- #[serde(rename = "deletedAt")]
134
- pub deleted_at: Option<chrono::DateTime<chrono::Utc>>,
135
- pub name: String,
136
- pub icon: String,
137
- #[serde(rename = "colorText")]
138
- pub color_text: HexColor,
139
- #[serde(rename = "colorBg")]
140
- pub color_bg: HexColor,
141
- }
142
-
143
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
144
- pub struct SubjectStatistics {
145
- pub id: i64,
146
- #[serde(rename = "accountId")]
147
- pub account_id: uuid::Uuid,
148
- #[serde(rename = "groupId")]
149
- pub group_id: i64,
150
- #[serde(rename = "createdAt")]
151
- pub created_at: chrono::DateTime<chrono::Utc>,
152
- #[serde(rename = "deletedAt")]
153
- pub deleted_at: Option<chrono::DateTime<chrono::Utc>>,
154
- pub name: String,
155
- pub icon: String,
156
- #[serde(rename = "colorText")]
157
- pub color_text: HexColor,
158
- #[serde(rename = "colorBg")]
159
- pub color_bg: HexColor,
160
- pub status: StudyStatus,
161
- #[serde(rename = "durationTotal")]
162
- pub duration_total: i64,
163
- #[serde(rename = "durationDay")]
164
- pub duration_day: i64,
165
- #[serde(rename = "durationWeek")]
166
- pub duration_week: i64,
167
- #[serde(rename = "durationMonth")]
168
- pub duration_month: i64,
169
- }
170
-
171
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
172
- pub struct CreateSubjectBody {
173
- pub name: String,
174
- pub icon: String,
175
- #[serde(rename = "colorText")]
176
- pub color_text: HexColor,
177
- #[serde(rename = "colorBg")]
178
- pub color_bg: HexColor,
179
- }
180
-
181
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
182
- pub struct UpdateSubjectBody {
183
- #[serde(skip_serializing_if = "Option::is_none")]
184
- pub name: Option<String>,
185
- #[serde(skip_serializing_if = "Option::is_none")]
186
- pub icon: Option<String>,
187
- #[serde(rename = "colorText")]
188
- #[serde(skip_serializing_if = "Option::is_none")]
189
- pub color_text: Option<HexColor>,
190
- #[serde(rename = "colorBg")]
191
- #[serde(skip_serializing_if = "Option::is_none")]
192
- pub color_bg: Option<HexColor>,
193
- }
194
-
195
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
196
- pub struct Log {
197
- pub id: i64,
198
- #[serde(rename = "accountId")]
199
- pub account_id: uuid::Uuid,
200
- #[serde(rename = "subjectId")]
201
- pub subject_id: i64,
202
- #[serde(rename = "startedAt")]
203
- pub started_at: chrono::DateTime<chrono::Utc>,
204
- #[serde(rename = "stoppedAt")]
205
- pub stopped_at: Option<chrono::DateTime<chrono::Utc>>,
206
- #[serde(rename = "logContent")]
207
- pub log_content: Option<String>,
208
- pub status: StudyStatus,
209
- }
210
-
211
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
212
- pub struct Timelog {
213
- pub id: i64,
214
- #[serde(rename = "accountId")]
215
- pub account_id: uuid::Uuid,
216
- #[serde(rename = "logId")]
217
- pub log_id: i64,
218
- #[serde(rename = "subjectId")]
219
- pub subject_id: i64,
220
- #[serde(rename = "startedAt")]
221
- pub started_at: chrono::DateTime<chrono::Utc>,
222
- #[serde(rename = "stoppedAt")]
223
- pub stopped_at: Option<chrono::DateTime<chrono::Utc>>,
224
- #[serde(rename = "durationInSeconds")]
225
- pub duration_in_seconds: i64,
226
- }
227
-
228
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
229
- pub struct LogWithTimelogs {
230
- pub id: i64,
231
- #[serde(rename = "accountId")]
232
- pub account_id: uuid::Uuid,
233
- #[serde(rename = "subjectId")]
234
- pub subject_id: i64,
235
- #[serde(rename = "startedAt")]
236
- pub started_at: chrono::DateTime<chrono::Utc>,
237
- #[serde(rename = "stoppedAt")]
238
- pub stopped_at: Option<chrono::DateTime<chrono::Utc>>,
239
- #[serde(rename = "logContent")]
240
- pub log_content: Option<String>,
241
- pub status: StudyStatus,
242
- pub timelogs: Vec<Timelog>,
243
- }
244
-
245
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
246
- pub struct SessionEvent {
247
- #[serde(rename = "eventType")]
248
- pub event_type: EnumSessionSessionpause4,
249
- pub session: LogWithTimelogs,
250
- }
251
-
252
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
253
- pub struct GroupEvent {
254
- #[serde(rename = "eventType")]
255
- pub event_type: EnumGroupcreatedGroupupdated3,
256
- pub group: Group,
257
- }
258
-
259
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
260
- pub struct SubjectEvent {
261
- #[serde(rename = "eventType")]
262
- pub event_type: EnumSubjectcreatedSubjectupdated4,
263
- pub subject: Subject,
264
- }
265
-
266
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
267
- pub struct SessionOpenedResponse {
268
- pub log: Log,
269
- pub timelog: Timelog,
270
- }
271
-
272
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
273
- pub struct SessionClosedResponse {
274
- pub log: Log,
275
- pub timelog: Timelog,
276
- }
277
-
278
- #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
279
- pub struct SessionNoteBody {
280
- #[serde(rename = "logContent")]
281
- #[serde(skip_serializing_if = "Option::is_none")]
282
- pub log_content: Option<String>,
283
- }
284
-
285
- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
286
- #[allow(clippy::enum_variant_names)]
287
- pub enum EnumSessionSessionpause4 {
288
- #[default]
289
- #[serde(rename = "session")]
290
- Session,
291
- #[serde(rename = "session_pause")]
292
- SessionPause,
293
- #[serde(rename = "session_resume")]
294
- SessionResume,
295
- #[serde(rename = "session_stop")]
296
- SessionStop,
297
- }
298
-
299
- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
300
- #[allow(clippy::enum_variant_names)]
301
- pub enum EnumGroupcreatedGroupupdated3 {
302
- #[default]
303
- #[serde(rename = "group_created")]
304
- GroupCreated,
305
- #[serde(rename = "group_updated")]
306
- GroupUpdated,
307
- #[serde(rename = "group_deleted")]
308
- GroupDeleted,
309
- }
310
-
311
- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
312
- #[allow(clippy::enum_variant_names)]
313
- pub enum EnumSubjectcreatedSubjectupdated4 {
314
- #[default]
315
- #[serde(rename = "subject_created")]
316
- SubjectCreated,
317
- #[serde(rename = "subject_updated")]
318
- SubjectUpdated,
319
- #[serde(rename = "subject_deleted")]
320
- SubjectDeleted,
321
- #[serde(rename = "subject_moved")]
322
- SubjectMoved,
323
- }
324
-
325
- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize, sqlx::Type)]
326
- #[sqlx(type_name = "study_status")]
327
- pub enum StudyStatus {
328
- #[default]
329
- #[serde(rename = "Starting")]
330
- Starting,
331
- #[serde(rename = "Paused")]
332
- Paused,
333
- #[serde(rename = "Stopped")]
334
- Stopped,
335
- }
336
-
337
- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
338
- pub enum ChartGranularity {
339
- #[default]
340
- #[serde(rename = "day")]
341
- Day,
342
- #[serde(rename = "week")]
343
- Week,
344
- #[serde(rename = "month")]
345
- Month,
346
- #[serde(rename = "year")]
347
- Year,
348
- }
349
-
350
13
  #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
351
14
  #[serde(untagged)]
352
- pub enum LearningEvents {
353
- Variant1(SessionEvent),
354
- Variant2(GroupEvent),
355
- Variant3(SubjectEvent),
356
- }
357
-
358
- pub type Uuid = uuid::Uuid;
359
-
360
- #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
361
- pub struct HexColor(pub String);
362
-
363
- impl TryFrom<String> for HexColor {
364
- type Error = String;
365
-
366
- fn try_from(value: String) -> Result<Self, Self::Error> {
367
- let re = regex::Regex::new(r"^#[0-9A-Fa-f]{6}$").unwrap();
368
- if re.is_match(&value) { Ok(Self(value)) } else { Err(format!("Invalid value: {}", value)) }
369
- }
15
+ pub enum MyEvent {
16
+ Variant1(MyEventData),
370
17
  }
371
18
 
@@ -1,5 +1,117 @@
1
1
  mod generated;
2
2
 
3
- fn main() {
4
- println!("Hello, world!");
3
+ use async_trait::async_trait;
4
+ use axum::response::IntoResponse;
5
+ use axum::response::sse::{Event, KeepAlive, Sse};
6
+ use generated::server::{EventsAccountsEventsResponse, Server};
7
+ use std::convert::Infallible;
8
+ use std::time::Duration;
9
+ use tokio::time::interval;
10
+ use tokio_stream::{StreamExt as _, wrappers::IntervalStream};
11
+
12
+ use crate::generated::server::PetsListResponse;
13
+
14
+ #[derive(Clone)]
15
+ struct AppState;
16
+
17
+ #[async_trait]
18
+ impl Server for AppState {
19
+ type Claims = ();
20
+
21
+ async fn events_accounts_events(
22
+ &self,
23
+ _account_id: String,
24
+ ) -> eyre::Result<EventsAccountsEventsResponse> {
25
+ let stream = IntervalStream::new(interval(Duration::from_millis(100)))
26
+ .map(|_| Ok::<_, Infallible>(Event::default().data("hi!")))
27
+ .take(3); // emit 3 events
28
+
29
+ let stream = Sse::new(stream).keep_alive(KeepAlive::default());
30
+ Ok(EventsAccountsEventsResponse::Ok(stream.into_response()))
31
+ }
32
+
33
+ async fn pets_list(
34
+ &self,
35
+ _first_query: String,
36
+ _second_query: String,
37
+ ) -> eyre::Result<PetsListResponse> {
38
+ todo!()
39
+ }
40
+ }
41
+
42
+ #[tokio::main]
43
+ async fn main() {
44
+ let app = generated::server::create_router(AppState, |r| r);
45
+ let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
46
+ println!("Server running on {}", listener.local_addr().unwrap());
47
+ axum::serve(listener, app).await.unwrap();
48
+ }
49
+
50
+ #[cfg(test)]
51
+ mod tests {
52
+ use super::*;
53
+ use axum::http::Request;
54
+ use http_body_util::BodyExt;
55
+ use tower::ServiceExt;
56
+
57
+ #[tokio::test]
58
+ async fn test_sse_endpoint() {
59
+ // Wait! The generated route in `example/output-rust/src/generated/server.rs` uses `/events/{accountId}` literally!
60
+ // We might need to use `/events/{accountId}` in the request to match, or fix the emitter if it's broken.
61
+ // Let's use whatever route is in the generated code to test the SSE handler logic first.
62
+ let app = generated::server::create_router(AppState, |r| r);
63
+
64
+ let req = Request::builder()
65
+ .uri("/events/123") // If the emitter generates {accountId}, axum won't match this if it requires `:accountId`.
66
+ // Wait, actually I should check if axum matches {accountId} or if it fails.
67
+ .body(axum::body::Body::empty())
68
+ .unwrap();
69
+
70
+ let response = app.oneshot(req).await.unwrap();
71
+
72
+ // If axum doesn't match, we will get 404. Let's handle both.
73
+ if response.status() == 404 {
74
+ let req2 = Request::builder()
75
+ .uri("/events/{accountId}")
76
+ .body(axum::body::Body::empty())
77
+ .unwrap();
78
+ let app = generated::server::create_router(AppState, |r| r);
79
+ let response2 = app.oneshot(req2).await.unwrap();
80
+ assert_eq!(response2.status(), 200);
81
+ return;
82
+ }
83
+
84
+ assert_eq!(response.status(), 200);
85
+ assert_eq!(
86
+ response.headers().get("content-type").unwrap(),
87
+ "text/event-stream"
88
+ );
89
+
90
+ // Read body frames sequentially
91
+ let mut body = response.into_body();
92
+ let mut timestamps = Vec::new();
93
+
94
+ while let Some(Ok(frame)) = body.frame().await {
95
+ if let Some(data) = frame.data_ref() {
96
+ let text = String::from_utf8_lossy(data);
97
+ if text.contains("data: hi!") {
98
+ timestamps.push(std::time::Instant::now());
99
+ }
100
+ }
101
+ }
102
+
103
+ // We should have received 3 events
104
+ assert_eq!(timestamps.len(), 3);
105
+
106
+ // Verify delay between events (approx 100ms)
107
+ for i in 1..timestamps.len() {
108
+ let diff = timestamps[i].duration_since(timestamps[i - 1]);
109
+ // Give it some slack for CI / slow execution, but ensure it's not instantaneous
110
+ assert!(
111
+ diff.as_millis() >= 80,
112
+ "Events were too fast! Diff: {}ms",
113
+ diff.as_millis()
114
+ );
115
+ }
116
+ }
5
117
  }
@@ -19,12 +19,14 @@
19
19
  }
20
20
  },
21
21
  "..": {
22
- "version": "0.10.3",
22
+ "version": "0.10.6",
23
23
  "license": "MIT",
24
24
  "devDependencies": {
25
25
  "@types/node": "latest",
26
26
  "@typespec/compiler": "latest",
27
+ "@typespec/events": "^0.81.0",
27
28
  "@typespec/http": "^1.11.0",
29
+ "@typespec/sse": "^0.81.0",
28
30
  "eslint": "^9.15.0",
29
31
  "prettier": "^3.3.3",
30
32
  "typescript": "^5.3.3",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-rust-emitter",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
4
4
  "description": "TypeSpec emitter that generates idiomatic Rust types and structs",
5
5
  "keywords": [
6
6
  "typespec",
package/src/emitter.ts CHANGED
@@ -393,7 +393,28 @@ function getOperationResponses(
393
393
  anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
394
394
  ): ResponseInfo[] {
395
395
  const responses: ResponseInfo[] = [];
396
- const returnType = operation.returnType;
396
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
397
+ const returnType = operation.returnType as any;
398
+
399
+ // Check for Array return type first (use runtime check since TS doesn't narrow Array)
400
+ if (
401
+ returnType.kind !== "Union" &&
402
+ returnType.kind !== "Model" &&
403
+ returnType.valueType
404
+ ) {
405
+ // This is an Array type
406
+ const { type: rustType } = getRustTypeForProperty(
407
+ returnType.valueType,
408
+ program,
409
+ anonymousEnums,
410
+ );
411
+ responses.push({
412
+ statusCode: 200,
413
+ bodyType: rustType,
414
+ bodyDescription: "",
415
+ });
416
+ return responses;
417
+ }
397
418
 
398
419
  if (returnType.kind === "Union") {
399
420
  const union = returnType as Union;
@@ -412,8 +433,7 @@ function getOperationResponses(
412
433
  if (model.name === "SSEStream") {
413
434
  responses.push({
414
435
  statusCode: 200,
415
- bodyType:
416
- "axum::response::sse::Sse<std::pin::Pin<Box<dyn futures::stream::Stream<Item = Result<axum::response::sse::Event, std::convert::Infallible>> + Send>>>",
436
+ bodyType: "axum::response::Response",
417
437
  bodyDescription: "Server-Sent Events stream",
418
438
  isSse: true,
419
439
  });
@@ -467,7 +487,7 @@ function getBodyFromResponse(
467
487
  const model = variant.type as Model;
468
488
  if (model.name === "SSEStream") {
469
489
  return {
470
- type: "axum::response::sse::Sse<std::pin::Pin<Box<dyn futures::stream::Stream<Item = Result<axum::response::sse::Event, std::convert::Infallible>> + Send>>>",
490
+ type: "axum::response::Response",
471
491
  description: "Server-Sent Events stream",
472
492
  isSse: true,
473
493
  };
@@ -851,7 +871,10 @@ function generateRouter(
851
871
  if (hasQueryParams && queryParams.length > 0) {
852
872
  const queryTypeName = `${toPascalCase(handlerFnName)}Query`;
853
873
  const queryFields = queryParams
854
- .map((p) => ` pub ${p.rustName}: ${p.rustType}`)
874
+ .map(
875
+ (p) =>
876
+ ` #[serde(rename = "${p.name}")]\n pub ${p.rustName}: ${p.rustType}`,
877
+ )
855
878
  .join(",\n");
856
879
  queryTypeStructs.push(
857
880
  `#[derive(Debug, Clone, serde::Deserialize)]\npub struct ${queryTypeName} {\n${queryFields}\n}`,
@@ -435,12 +435,7 @@ describe("Rust emitter", () => {
435
435
  }
436
436
  `);
437
437
  const server = results["server.rs"];
438
- strictEqual(
439
- server.includes(
440
- "axum::response::sse::Sse<std::pin::Pin<Box<dyn futures::stream::Stream<Item = Result<axum::response::sse::Event, std::convert::Infallible>> + Send>>>",
441
- ),
442
- true,
443
- );
438
+ strictEqual(server.includes("axum::response::Response"), true);
444
439
  strictEqual(
445
440
  server.includes(
446
441
  "EventsStreamResponse::Ok(body) => body.into_response(),",