typespec-rust-emitter 0.10.2 → 0.10.5

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/src/emitter.ts CHANGED
@@ -194,6 +194,7 @@ interface ResponseInfo {
194
194
  statusCode: number;
195
195
  bodyType: string | undefined;
196
196
  bodyDescription: string | undefined;
197
+ isSse?: boolean;
197
198
  }
198
199
 
199
200
  function getDecoratorName(decorator: {
@@ -403,10 +404,21 @@ function getOperationResponses(
403
404
  statusCode,
404
405
  bodyType: bodyInfo.type,
405
406
  bodyDescription: bodyInfo.description,
407
+ isSse: bodyInfo.isSse,
406
408
  });
407
409
  }
408
410
  } else if (returnType.kind === "Model") {
409
411
  const model = returnType as Model;
412
+ if (model.name === "SSEStream") {
413
+ responses.push({
414
+ 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>>>",
417
+ bodyDescription: "Server-Sent Events stream",
418
+ isSse: true,
419
+ });
420
+ return responses;
421
+ }
410
422
  for (const [propName, prop] of model.properties) {
411
423
  if (propName === "body") {
412
424
  const { type: rustType } = getRustTypeForProperty(
@@ -446,9 +458,20 @@ function getBodyFromResponse(
446
458
  variant: { type: Type },
447
459
  program: Program,
448
460
  anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
449
- ): { type: string | undefined; description: string | undefined } {
461
+ ): {
462
+ type: string | undefined;
463
+ description: string | undefined;
464
+ isSse?: boolean;
465
+ } {
450
466
  if (variant.type.kind === "Model") {
451
467
  const model = variant.type as Model;
468
+ if (model.name === "SSEStream") {
469
+ 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>>>",
471
+ description: "Server-Sent Events stream",
472
+ isSse: true,
473
+ };
474
+ }
452
475
  for (const [_propName, prop] of model.properties) {
453
476
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
454
477
  const decorators = (prop as any).decorators;
@@ -639,6 +662,13 @@ pub trait Server: Send + Sync {
639
662
  }
640
663
  }
641
664
 
665
+ // Add query parameters
666
+ for (const param of opInfo.parameters) {
667
+ if (param.location === "query") {
668
+ paramParts.push(`${param.rustName}: ${param.rustType}`);
669
+ }
670
+ }
671
+
642
672
  // Add body parameter
643
673
  if (opInfo.body) {
644
674
  const bodyType = getRustTypeForProperty(
@@ -705,11 +735,14 @@ function generateResponseEnums(
705
735
  const variantName = getStatusVariantName(resp.statusCode);
706
736
  if (!resp.bodyType) {
707
737
  variants.push(` ${variantName},`);
738
+ } else if (resp.isSse) {
739
+ variants.push(` ${variantName}(${resp.bodyType}),`);
708
740
  } else {
709
741
  variants.push(` ${variantName}(Json<${resp.bodyType}>),`);
710
742
  }
711
743
  }
712
- parts.push(`pub enum ${responseName} {
744
+ parts.push(`#[allow(clippy::type_complexity)]
745
+ pub enum ${responseName} {
713
746
  ${variants.join("\n")}
714
747
  }
715
748
  `);
@@ -725,6 +758,10 @@ ${variants.join("\n")}
725
758
  parts.push(
726
759
  ` ${responseName}::${variantName} => ${statusCodeStr}.into_response(),`,
727
760
  );
761
+ } else if (resp.isSse) {
762
+ parts.push(
763
+ ` ${responseName}::${variantName}(body) => body.into_response(),`,
764
+ );
728
765
  } else {
729
766
  parts.push(
730
767
  ` ${responseName}::${variantName}(body) => (${statusCodeStr}, body).into_response(),`,
@@ -747,6 +784,7 @@ function generateRouter(
747
784
  anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
748
785
  ): string {
749
786
  const handlers: string[] = [];
787
+ const queryTypeStructs: string[] = [];
750
788
  const publicRoutes: string[] = [];
751
789
  const protectedRoutes: string[] = [];
752
790
  const usedMethods = new Set<string>();
@@ -772,6 +810,10 @@ function generateRouter(
772
810
 
773
811
  const pathParams = opInfo.parameters.filter((p) => p.location === "path");
774
812
  const hasPathParams = pathParams.length > 0;
813
+ const queryParams = opInfo.parameters.filter(
814
+ (p) => p.location === "query",
815
+ );
816
+ const hasQueryParams = queryParams.length > 0;
775
817
  const hasBody = !!opInfo.body;
776
818
 
777
819
  // Build extractor lines and server method call arguments
@@ -805,6 +847,21 @@ function generateRouter(
805
847
  }
806
848
  }
807
849
 
850
+ // Query params come after Path
851
+ if (hasQueryParams && queryParams.length > 0) {
852
+ const queryTypeName = `${toPascalCase(handlerFnName)}Query`;
853
+ const queryFields = queryParams
854
+ .map((p) => ` pub ${p.rustName}: ${p.rustType}`)
855
+ .join(",\n");
856
+ queryTypeStructs.push(
857
+ `#[derive(Debug, Clone, serde::Deserialize)]\npub struct ${queryTypeName} {\n${queryFields}\n}`,
858
+ );
859
+ extractorLines.push(` Query(params): Query<${queryTypeName}>,`);
860
+ for (const param of queryParams) {
861
+ serverArgs.push(`params.${param.rustName}`);
862
+ }
863
+ }
864
+
808
865
  // Json body comes last
809
866
  if (hasBody && opInfo.body) {
810
867
  const bodyType = getRustTypeForProperty(
@@ -859,10 +916,15 @@ where
859
916
  const routerBody = buildRouterBody(publicRoutes, protectedRoutes);
860
917
 
861
918
  const parts: string[] = [];
862
- parts.push(`use axum::routing::{${methodImports}};
919
+ parts.push(`use axum::extract::Query;
920
+ use axum::routing::{${methodImports}};
863
921
  use axum::Router;
864
922
 
865
923
  `);
924
+ if (queryTypeStructs.length > 0) {
925
+ parts.push(queryTypeStructs.join("\n\n"));
926
+ parts.push("\n\n");
927
+ }
866
928
  parts.push(handlers.join("\n\n"));
867
929
  parts.push(`
868
930
  pub fn create_router<S, M>(service: S, middleware: M) -> Router
@@ -1026,7 +1088,9 @@ function getRustTypeForProperty(
1026
1088
  };
1027
1089
  }
1028
1090
  const values = variants.map((v) => (v.type as StringLiteral).value);
1029
- const enumName = `Enum_${values.slice(0, 2).join("_")}_${values.length}`;
1091
+ const sanitized = values.map((v) => v.replace(/_/g, ""));
1092
+ const firstTwo = sanitized.slice(0, 2).map(toPascalCase).join("");
1093
+ const enumName = `Enum${firstTwo}${sanitized.length}`;
1030
1094
  if (!anonymousEnums.has(enumName)) {
1031
1095
  anonymousEnums.set(enumName, {
1032
1096
  enumName,
@@ -1179,12 +1243,12 @@ function emitStringLiteralUnion(union: Union): string {
1179
1243
  const variants = Array.from(union.variants.values());
1180
1244
 
1181
1245
  parts.push(
1182
- `#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]\npub enum ${name} {`,
1246
+ `#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]\n#[allow(clippy::enum_variant_names)]\npub enum ${name} {`,
1183
1247
  );
1184
1248
 
1185
1249
  for (let i = 0; i < variants.length; i++) {
1186
1250
  const literalType = variants[i].type as StringLiteral;
1187
- const variantName = toRustVariantName(literalType.value);
1251
+ const variantName = toPascalCase(literalType.value);
1188
1252
  const serdeValue = literalType.value;
1189
1253
  if (i === 0) {
1190
1254
  parts.push(` #[default]`);
@@ -1515,11 +1579,11 @@ export async function $onEmit(
1515
1579
  for (const [enumName, anonEnum] of anonymousEnums) {
1516
1580
  const parts: string[] = [];
1517
1581
  parts.push(
1518
- `#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]\npub enum ${enumName} {`,
1582
+ `#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]\n#[allow(clippy::enum_variant_names)]\npub enum ${enumName} {`,
1519
1583
  );
1520
1584
  for (let i = 0; i < anonEnum.variants.length; i++) {
1521
1585
  const literal = anonEnum.variants[i];
1522
- const variantName = toRustVariantName(literal.value);
1586
+ const variantName = toPascalCase(literal.value);
1523
1587
  if (i === 0) {
1524
1588
  parts.push(` #[default]`);
1525
1589
  }
@@ -1571,6 +1635,11 @@ export async function $onEmit(
1571
1635
  content: outputContent,
1572
1636
  });
1573
1637
 
1638
+ await emitFile(context.program, {
1639
+ path: resolvePath(context.emitterOutputDir, "mod.rs"),
1640
+ content: "pub mod server;\npub mod types;\n",
1641
+ });
1642
+
1574
1643
  if (namespaceGroups.length > 0) {
1575
1644
  const serverTrait = generateServerTrait(
1576
1645
  context.program,
@@ -50,6 +50,17 @@ describe("Rust emitter", () => {
50
50
  strictEqual(output.includes("Pending,"), true);
51
51
  });
52
52
 
53
+ it("emits anonymous string enum without underscores", async () => {
54
+ const results = await emit(`
55
+ model Event {
56
+ eventType: "event_one" | "event_two" | "event_three",
57
+ }
58
+ `);
59
+ const output = results["types.rs"];
60
+ const badNameContainsUnderscore = output.includes("Enum_event_one_");
61
+ strictEqual(badNameContainsUnderscore, false);
62
+ });
63
+
53
64
  it("emits integer enum", async () => {
54
65
  const results = await emit(`
55
66
  enum Priority {
@@ -369,4 +380,72 @@ describe("Rust emitter", () => {
369
380
  const output = results["types.rs"];
370
381
  strictEqual(output.includes("impl IntoResponse for ApiError"), false);
371
382
  });
383
+
384
+ it("emits query parameters in handlers", async () => {
385
+ const results = await emit(`
386
+ import "@typespec/http";
387
+ using TypeSpec.Http;
388
+
389
+ model CalendarItem {
390
+ date: string;
391
+ }
392
+
393
+ @route("/calendar/{accountId}")
394
+ namespace Calendar {
395
+ @get
396
+ @tag("Calendar")
397
+ op getCalendar(
398
+ @path accountId: string,
399
+ @query year: int32,
400
+ @query month: int32,
401
+ ): {
402
+ @statusCode statusCode: 200;
403
+ @body body: CalendarItem[];
404
+ };
405
+ }
406
+ `);
407
+ const server = results["server.rs"];
408
+ strictEqual(server.includes("Query(params): Query"), true);
409
+ strictEqual(server.includes("pub year: i32"), true);
410
+ strictEqual(server.includes("pub month: i32"), true);
411
+ strictEqual(server.includes("params.year"), true);
412
+ strictEqual(server.includes("params.month"), true);
413
+ });
414
+ it("emits SSEStream properly", async () => {
415
+ const results = await emit(`
416
+ import "@typespec/http";
417
+ import "@typespec/sse";
418
+ import "@typespec/events";
419
+ using TypeSpec.Http;
420
+ using TypeSpec.SSE;
421
+
422
+ model TestEventData {
423
+ data: string;
424
+ }
425
+
426
+ @TypeSpec.Events.events
427
+ union TestEvent {
428
+ data: TestEventData
429
+ }
430
+
431
+ @route("/events")
432
+ namespace Events {
433
+ @get
434
+ op stream(): SSEStream<TestEvent>;
435
+ }
436
+ `);
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
+ );
444
+ strictEqual(
445
+ server.includes(
446
+ "EventsStreamResponse::Ok(body) => body.into_response(),",
447
+ ),
448
+ true,
449
+ );
450
+ });
372
451
  });
package/test/test-host.ts CHANGED
@@ -3,7 +3,12 @@ import { expectDiagnosticEmpty } from "@typespec/compiler/testing";
3
3
  import { createTester } from "@typespec/compiler/testing";
4
4
 
5
5
  export const Tester = createTester(resolvePath(import.meta.dirname, "../.."), {
6
- libraries: ["typespec-rust-emitter"],
6
+ libraries: [
7
+ "@typespec/http",
8
+ "@typespec/sse",
9
+ "@typespec/events",
10
+ "typespec-rust-emitter",
11
+ ],
7
12
  }).emit("typespec-rust-emitter");
8
13
 
9
14
  export async function emitWithDiagnostics(