typespec-rust-emitter 0.12.0 → 0.13.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.
Files changed (138) hide show
  1. package/AGENTS.md +82 -80
  2. package/CHANGELOG.md +17 -0
  3. package/dist/src/decorators/cache_control.d.ts +6 -0
  4. package/dist/src/decorators/cache_control.js +9 -0
  5. package/dist/src/decorators/cache_control.js.map +1 -0
  6. package/dist/src/decorators/etag_cache.d.ts +6 -0
  7. package/dist/src/decorators/etag_cache.js +9 -0
  8. package/dist/src/decorators/etag_cache.js.map +1 -0
  9. package/dist/src/decorators/index.d.ts +6 -0
  10. package/dist/src/decorators/index.js +7 -0
  11. package/dist/src/decorators/index.js.map +1 -0
  12. package/dist/src/decorators/rust_attr.d.ts +3 -0
  13. package/dist/src/decorators/rust_attr.js +45 -0
  14. package/dist/src/decorators/rust_attr.js.map +1 -0
  15. package/dist/src/decorators/rust_derive.d.ts +3 -0
  16. package/dist/src/decorators/rust_derive.js +39 -0
  17. package/dist/src/decorators/rust_derive.js.map +1 -0
  18. package/dist/src/decorators/rust_impl.d.ts +2 -0
  19. package/dist/src/decorators/rust_impl.js +19 -0
  20. package/dist/src/decorators/rust_impl.js.map +1 -0
  21. package/dist/src/decorators/rust_self.d.ts +3 -0
  22. package/dist/src/decorators/rust_self.js +35 -0
  23. package/dist/src/decorators/rust_self.js.map +1 -0
  24. package/dist/src/emitter.d.ts +2 -11
  25. package/dist/src/emitter.js +7 -1282
  26. package/dist/src/emitter.js.map +1 -1
  27. package/dist/src/formatter/index.d.ts +2 -0
  28. package/dist/src/formatter/index.js +3 -0
  29. package/dist/src/formatter/index.js.map +1 -0
  30. package/dist/src/formatter/mappings.d.ts +4 -0
  31. package/dist/src/formatter/mappings.js +68 -0
  32. package/dist/src/formatter/mappings.js.map +1 -0
  33. package/dist/src/formatter/strings.d.ts +4 -0
  34. package/dist/src/formatter/strings.js +32 -0
  35. package/dist/src/formatter/strings.js.map +1 -0
  36. package/dist/src/generator/etag_router.d.ts +30 -0
  37. package/dist/src/generator/etag_router.js +123 -0
  38. package/dist/src/generator/etag_router.js.map +1 -0
  39. package/dist/src/generator/index.d.ts +5 -0
  40. package/dist/src/generator/index.js +6 -0
  41. package/dist/src/generator/index.js.map +1 -0
  42. package/dist/src/generator/response_enums.d.ts +6 -0
  43. package/dist/src/generator/response_enums.js +58 -0
  44. package/dist/src/generator/response_enums.js.map +1 -0
  45. package/dist/src/generator/router.d.ts +7 -0
  46. package/dist/src/generator/router.js +227 -0
  47. package/dist/src/generator/router.js.map +1 -0
  48. package/dist/src/generator/server_trait.d.ts +6 -0
  49. package/dist/src/generator/server_trait.js +97 -0
  50. package/dist/src/generator/server_trait.js.map +1 -0
  51. package/dist/src/generator/types_file.d.ts +11 -0
  52. package/dist/src/generator/types_file.js +209 -0
  53. package/dist/src/generator/types_file.js.map +1 -0
  54. package/dist/src/index.d.ts +1 -1
  55. package/dist/src/index.js +1 -1
  56. package/dist/src/index.js.map +1 -1
  57. package/dist/src/lib.js +1 -1
  58. package/dist/src/lib.js.map +1 -1
  59. package/dist/src/models/index.d.ts +2 -0
  60. package/dist/src/models/index.js +3 -0
  61. package/dist/src/models/index.js.map +1 -0
  62. package/dist/src/models/keys.d.ts +6 -0
  63. package/dist/src/models/keys.js +8 -0
  64. package/dist/src/models/keys.js.map +1 -0
  65. package/dist/src/models/types.d.ts +45 -0
  66. package/dist/src/models/types.js +2 -0
  67. package/dist/src/models/types.js.map +1 -0
  68. package/dist/src/parser/decorators.d.ts +18 -0
  69. package/dist/src/parser/decorators.js +28 -0
  70. package/dist/src/parser/decorators.js.map +1 -0
  71. package/dist/src/parser/index.d.ts +6 -0
  72. package/dist/src/parser/index.js +7 -0
  73. package/dist/src/parser/index.js.map +1 -0
  74. package/dist/src/parser/operations.d.ts +13 -0
  75. package/dist/src/parser/operations.js +127 -0
  76. package/dist/src/parser/operations.js.map +1 -0
  77. package/dist/src/parser/parameters.d.ts +5 -0
  78. package/dist/src/parser/parameters.js +98 -0
  79. package/dist/src/parser/parameters.js.map +1 -0
  80. package/dist/src/parser/responses.d.ts +13 -0
  81. package/dist/src/parser/responses.js +132 -0
  82. package/dist/src/parser/responses.js.map +1 -0
  83. package/dist/src/parser/routes.d.ts +4 -0
  84. package/dist/src/parser/routes.js +36 -0
  85. package/dist/src/parser/routes.js.map +1 -0
  86. package/dist/src/parser/types.d.ts +9 -0
  87. package/dist/src/parser/types.js +157 -0
  88. package/dist/src/parser/types.js.map +1 -0
  89. package/dist/test/etag_cache.test.d.ts +1 -0
  90. package/dist/test/etag_cache.test.js +62 -0
  91. package/dist/test/etag_cache.test.js.map +1 -0
  92. package/dist/test/test-host.d.ts +11 -0
  93. package/dist/test/test-host.js +28 -0
  94. package/dist/test/test-host.js.map +1 -1
  95. package/example/main.tsp +27 -1
  96. package/example/output-rust/Cargo.lock +48 -0
  97. package/example/output-rust/Cargo.toml +1 -0
  98. package/example/output-rust/src/generated/server.rs +122 -11
  99. package/example/output-rust/src/generated/types.rs +6 -0
  100. package/example/output-rust/src/main.rs +60 -27
  101. package/justfile +31 -2
  102. package/package.json +1 -1
  103. package/scripts/update-golden.js +36 -0
  104. package/src/decorators/cache_control.ts +14 -0
  105. package/src/decorators/etag_cache.ts +14 -0
  106. package/src/decorators/index.ts +6 -0
  107. package/src/decorators/rust_attr.ts +61 -0
  108. package/src/decorators/rust_derive.ts +55 -0
  109. package/src/decorators/rust_impl.ts +29 -0
  110. package/src/decorators/rust_self.ts +42 -0
  111. package/src/emitter.ts +18 -1654
  112. package/src/formatter/index.ts +2 -0
  113. package/src/formatter/mappings.ts +70 -0
  114. package/src/formatter/strings.ts +33 -0
  115. package/src/generator/etag_router.ts +147 -0
  116. package/src/generator/index.ts +5 -0
  117. package/src/generator/response_enums.ts +76 -0
  118. package/src/generator/router.ts +280 -0
  119. package/src/generator/server_trait.ts +134 -0
  120. package/src/generator/types_file.ts +297 -0
  121. package/src/index.ts +3 -1
  122. package/src/lib.ts +1 -1
  123. package/src/lib.tsp +3 -1
  124. package/src/models/index.ts +2 -0
  125. package/src/models/keys.ts +7 -0
  126. package/src/models/types.ts +54 -0
  127. package/src/parser/decorators.ts +34 -0
  128. package/src/parser/index.ts +6 -0
  129. package/src/parser/operations.ts +158 -0
  130. package/src/parser/parameters.ts +117 -0
  131. package/src/parser/responses.ts +170 -0
  132. package/src/parser/routes.ts +47 -0
  133. package/src/parser/types.ts +215 -0
  134. package/test/etag_cache.test.ts +69 -0
  135. package/test/golden/etag_cache/server.rs +109 -0
  136. package/test/golden/etag_cache/spec.tsp +20 -0
  137. package/test/golden/etag_cache/types.rs +13 -0
  138. package/test/test-host.ts +43 -0
@@ -0,0 +1,157 @@
1
+ import { getFormat, getPattern, isArrayModelType, isRecordModelType, } from "@typespec/compiler";
2
+ import { formatToRust, scalarToRust } from "../formatter/mappings.js";
3
+ import { toPascalCase } from "../formatter/strings.js";
4
+ const typeSpecNamespaces = new Set([
5
+ "TypeSpec",
6
+ "@typespec/http",
7
+ "@typespec/rest",
8
+ "@typespec/openapi",
9
+ "@typespec/openapi3",
10
+ "@typespec/json-schema",
11
+ ]);
12
+ export function isStdLibNamespace(ns) {
13
+ if (!ns)
14
+ return false;
15
+ const fullName = ns.name;
16
+ if (typeSpecNamespaces.has(fullName))
17
+ return true;
18
+ if (ns.namespace)
19
+ return isStdLibNamespace(ns.namespace);
20
+ return false;
21
+ }
22
+ export function isStdLibType(type) {
23
+ if ("namespace" in type) {
24
+ const ns = type.namespace;
25
+ if (isStdLibNamespace(ns))
26
+ return true;
27
+ }
28
+ return false;
29
+ }
30
+ export function getRustTypeForProperty(type, program, anonymousEnums) {
31
+ const kind = type.kind;
32
+ if (kind === "Model") {
33
+ const model = type;
34
+ if (isArrayModelType(model) && model.indexer?.value) {
35
+ const element = getRustTypeForProperty(model.indexer.value, program, anonymousEnums);
36
+ return { type: `Vec<${element.type}>`, isStringLiteral: false };
37
+ }
38
+ if (isRecordModelType(model) && model.indexer?.value) {
39
+ const value = getRustTypeForProperty(model.indexer.value, program, anonymousEnums);
40
+ return {
41
+ type: `std::collections::HashMap<String, ${value.type}>`,
42
+ isStringLiteral: false,
43
+ };
44
+ }
45
+ return { type: toPascalCase(model.name), isStringLiteral: false };
46
+ }
47
+ if (kind === "ModelProperty") {
48
+ return getRustTypeForProperty(type.type, program, anonymousEnums);
49
+ }
50
+ if (kind === "Enum") {
51
+ return { type: toPascalCase(type.name), isStringLiteral: false };
52
+ }
53
+ if (kind === "Union") {
54
+ const unionType = type;
55
+ const variants = Array.from(unionType.variants.values());
56
+ const allStringLiterals = variants.every((v) => v.type.kind === "String");
57
+ if (allStringLiterals) {
58
+ if (unionType.name) {
59
+ return {
60
+ type: toPascalCase(unionType.name),
61
+ isStringLiteral: false,
62
+ };
63
+ }
64
+ const values = variants.map((v) => v.type.value);
65
+ const sanitized = values.map((v) => v.replace(/_/g, ""));
66
+ const firstTwo = sanitized.slice(0, 2).map(toPascalCase).join("");
67
+ const enumName = `Enum${firstTwo}${sanitized.length}`;
68
+ if (!anonymousEnums.has(enumName)) {
69
+ anonymousEnums.set(enumName, {
70
+ enumName,
71
+ variants: variants.map((v) => v.type),
72
+ });
73
+ }
74
+ return { type: enumName, isStringLiteral: false };
75
+ }
76
+ const nonNullVariants = variants.filter((v) => {
77
+ const vt = v.type;
78
+ if (vt.kind === "Null")
79
+ return false;
80
+ if (vt.kind === "Intrinsic" &&
81
+ vt.name === "null")
82
+ return false;
83
+ return true;
84
+ });
85
+ if (nonNullVariants.length === 1 &&
86
+ (variants.length === 2 ||
87
+ variants.some((v) => {
88
+ const vt = v.type;
89
+ return (vt.kind === "Null" ||
90
+ (vt.kind === "Intrinsic" &&
91
+ vt.name === "null"));
92
+ }))) {
93
+ const vt = getRustTypeForProperty(nonNullVariants[0].type, program, anonymousEnums);
94
+ return { type: `Option<${vt.type}>`, isStringLiteral: false };
95
+ }
96
+ const variantStrings = [];
97
+ for (const v of variants) {
98
+ const vt = v.type;
99
+ if (vt.kind === "Null")
100
+ continue;
101
+ if (vt.kind === "Intrinsic" &&
102
+ vt.name === "null")
103
+ continue;
104
+ const rustVt = getRustTypeForProperty(vt, program, anonymousEnums);
105
+ variantStrings.push(rustVt.type);
106
+ }
107
+ const uniqueTypes = [...new Set(variantStrings)];
108
+ const resultType = uniqueTypes.length === 1
109
+ ? uniqueTypes[0]
110
+ : `(${uniqueTypes.join(" | ")})`;
111
+ return { type: resultType, isStringLiteral: false };
112
+ }
113
+ if (kind === "Scalar") {
114
+ const scalar = type;
115
+ const format = getFormat(program, scalar);
116
+ const pattern = getPattern(program, scalar);
117
+ if (format && formatToRust[format] && !pattern) {
118
+ return { type: formatToRust[format], isStringLiteral: false };
119
+ }
120
+ if (pattern) {
121
+ return { type: toPascalCase(scalar.name), isStringLiteral: false };
122
+ }
123
+ return {
124
+ type: scalarToRust[scalar.name] ?? scalar.name,
125
+ isStringLiteral: false,
126
+ };
127
+ }
128
+ if (kind === "Intrinsic") {
129
+ const intrinsic = type;
130
+ const format = getFormat(program, type);
131
+ if (format && formatToRust[format]) {
132
+ return { type: formatToRust[format], isStringLiteral: false };
133
+ }
134
+ return {
135
+ type: scalarToRust[intrinsic.name] ?? "serde_json::Value",
136
+ isStringLiteral: false,
137
+ };
138
+ }
139
+ if (kind === "String") {
140
+ return { type: "String", isStringLiteral: false };
141
+ }
142
+ if (kind === "StringLiteral") {
143
+ return {
144
+ type: "String",
145
+ isStringLiteral: true,
146
+ stringLiteralValue: type.value,
147
+ };
148
+ }
149
+ if (kind === "Boolean" || kind === "BooleanLiteral") {
150
+ return { type: "bool", isStringLiteral: false };
151
+ }
152
+ if (kind === "Number" || kind === "NumericLiteral") {
153
+ return { type: "f64", isStringLiteral: false };
154
+ }
155
+ return { type: "serde_json::Value", isStringLiteral: false };
156
+ }
157
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/parser/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ import { strictEqual } from "node:assert";
2
+ import { describe, it } from "node:test";
3
+ import { emit, compareWithGolden } from "./test-host.js";
4
+ const ETAG_SPEC = `
5
+ import "@typespec/http";
6
+ import "typespec-rust-emitter";
7
+ using TypeSpec.Http;
8
+
9
+ model Article { id: string; title: string; }
10
+
11
+ @route("/articles")
12
+ namespace Articles {
13
+ @etagCache
14
+ @get
15
+ op getArticle(@path id: string): {
16
+ @statusCode statusCode: 200;
17
+ @body body: Article;
18
+ } | {
19
+ @statusCode statusCode: 304;
20
+ };
21
+ }
22
+ `;
23
+ describe("@etagCache decorator", () => {
24
+ it("generates EtagCache trait in server.rs", async () => {
25
+ const results = await emit(ETAG_SPEC);
26
+ const server = results["server.rs"];
27
+ strictEqual(server.includes("pub trait EtagCache"), true);
28
+ strictEqual(server.includes("fn get(&self, key: &str) -> Option<String>"), true);
29
+ strictEqual(server.includes("fn set(&self, key: &str, etag: &str)"), true);
30
+ });
31
+ it("generates cache-aware handler with 304 short-circuit", async () => {
32
+ const results = await emit(ETAG_SPEC);
33
+ const server = results["server.rs"];
34
+ strictEqual(server.includes("axum::http::StatusCode::NOT_MODIFIED"), true);
35
+ strictEqual(server.includes("cache.get(cache_key.as_ref())"), true);
36
+ });
37
+ it("supports custom etagKey and @cacheControl", async () => {
38
+ const spec = `
39
+ import "@typespec/http";
40
+ import "typespec-rust-emitter";
41
+ using TypeSpec.Http;
42
+
43
+ @route("/test")
44
+ namespace Test {
45
+ @etagCache("my-key")
46
+ @cacheControl("public")
47
+ @get
48
+ op run(): { @statusCode statusCode: 200; };
49
+ }
50
+ `;
51
+ const results = await emit(spec);
52
+ const server = results["server.rs"];
53
+ strictEqual(server.includes('let cache_key = "my-key";'), true);
54
+ strictEqual(server.includes('axum::http::header::CACHE_CONTROL'), true);
55
+ strictEqual(server.includes('"public"'), true);
56
+ });
57
+ it("matches golden file", async () => {
58
+ const results = await emit(ETAG_SPEC);
59
+ compareWithGolden(results, "etag_cache", "server.rs");
60
+ });
61
+ });
62
+ //# sourceMappingURL=etag_cache.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"etag_cache.test.js","sourceRoot":"","sources":["../../test/etag_cache.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEzD,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;CAkBjB,CAAC;AAEF,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACtD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC;QACtC,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;QACpC,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,IAAI,CAAC,CAAC;QAC1D,WAAW,CACT,MAAM,CAAC,QAAQ,CAAC,4CAA4C,CAAC,EAC7D,IAAI,CACL,CAAC;QACF,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,sCAAsC,CAAC,EAAE,IAAI,CAAC,CAAC;IAC7E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACpE,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC;QACtC,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;QACpC,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,sCAAsC,CAAC,EAAE,IAAI,CAAC,CAAC;QAC3E,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,+BAA+B,CAAC,EAAE,IAAI,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,KAAK,IAAI,EAAE;QACzD,MAAM,IAAI,GAAG;;;;;;;;;;;;KAYZ,CAAC;QACF,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;QACpC,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,2BAA2B,CAAC,EAAE,IAAI,CAAC,CAAC;QAChE,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,mCAAmC,CAAC,EAAE,IAAI,CAAC,CAAC;QACxE,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qBAAqB,EAAE,KAAK,IAAI,EAAE;QACnC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC;QACtC,iBAAiB,CAAC,OAAO,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -2,3 +2,14 @@ import { Diagnostic } from "@typespec/compiler";
2
2
  export declare const Tester: import("@typespec/compiler/testing").EmitterTester<import("@typespec/compiler/testing").TestEmitterCompileResult>;
3
3
  export declare function emitWithDiagnostics(code: string): Promise<[Record<string, string>, readonly Diagnostic[]]>;
4
4
  export declare function emit(code: string): Promise<Record<string, string>>;
5
+ /**
6
+ * compareWithGolden
7
+ *
8
+ * Reads an expected file from test/golden/<goldenDir>/<fileName>
9
+ * and asserts that the emitted output matches it exactly.
10
+ *
11
+ * @param emitted - Record produced by emit() e.g. { "types.rs": "...", "server.rs": "..." }
12
+ * @param goldenDir - Subdirectory name inside test/golden/
13
+ * @param fileName - File key to check, e.g. "server.rs"
14
+ */
15
+ export declare function compareWithGolden(emitted: Record<string, string>, goldenDir: string, fileName: string): void;
@@ -1,6 +1,7 @@
1
1
  import { resolvePath } from "@typespec/compiler";
2
2
  import { expectDiagnosticEmpty } from "@typespec/compiler/testing";
3
3
  import { createTester } from "@typespec/compiler/testing";
4
+ import { readFileSync } from "node:fs";
4
5
  export const Tester = createTester(resolvePath(import.meta.dirname, "../.."), {
5
6
  libraries: [
6
7
  "@typespec/http",
@@ -18,4 +19,31 @@ export async function emit(code) {
18
19
  expectDiagnosticEmpty(diagnostics);
19
20
  return result;
20
21
  }
22
+ /**
23
+ * compareWithGolden
24
+ *
25
+ * Reads an expected file from test/golden/<goldenDir>/<fileName>
26
+ * and asserts that the emitted output matches it exactly.
27
+ *
28
+ * @param emitted - Record produced by emit() e.g. { "types.rs": "...", "server.rs": "..." }
29
+ * @param goldenDir - Subdirectory name inside test/golden/
30
+ * @param fileName - File key to check, e.g. "server.rs"
31
+ */
32
+ export function compareWithGolden(emitted, goldenDir, fileName) {
33
+ const goldenPath = resolvePath(import.meta.dirname, "../../test", "golden", goldenDir, fileName);
34
+ const expected = readFileSync(goldenPath, "utf8");
35
+ const actual = emitted[fileName];
36
+ if (actual === undefined) {
37
+ throw new Error(`Emitter did not produce "${fileName}". Keys: ${Object.keys(emitted).join(", ")}`);
38
+ }
39
+ if (actual !== expected) {
40
+ // Show a useful diff-style message.
41
+ const expectedLines = expected.split("\n");
42
+ const actualLines = actual.split("\n");
43
+ const firstDiff = expectedLines.findIndex((l, i) => l !== actualLines[i]);
44
+ throw new Error(`Golden mismatch in ${goldenDir}/${fileName} at line ${firstDiff + 1}.\n` +
45
+ `Expected: ${expectedLines[firstDiff]}\n` +
46
+ `Actual: ${actualLines[firstDiff]}`);
47
+ }
48
+ }
21
49
  //# sourceMappingURL=test-host.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"test-host.js","sourceRoot":"","sources":["../../test/test-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE1D,MAAM,CAAC,MAAM,MAAM,GAAG,YAAY,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE;IAC5E,SAAS,EAAE;QACT,gBAAgB;QAChB,eAAe;QACf,kBAAkB;QAClB,uBAAuB;KACxB;CACF,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;AAEjC,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAY;IAEZ,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,WAAW,CAAC,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACzE,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAY;IACrC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC9D,qBAAqB,CAAC,WAAW,CAAC,CAAC;IACnC,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"test-host.js","sourceRoot":"","sources":["../../test/test-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,CAAC,MAAM,MAAM,GAAG,YAAY,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE;IAC5E,SAAS,EAAE;QACT,gBAAgB;QAChB,eAAe;QACf,kBAAkB;QAClB,uBAAuB;KACxB;CACF,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;AAEjC,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAY;IAEZ,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,WAAW,CAAC,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACzE,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAY;IACrC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC9D,qBAAqB,CAAC,WAAW,CAAC,CAAC;IACnC,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAA+B,EAC/B,SAAiB,EACjB,QAAgB;IAEhB,MAAM,UAAU,GAAG,WAAW,CAC5B,MAAM,CAAC,IAAI,CAAC,OAAO,EACnB,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,QAAQ,CACT,CAAC;IACF,MAAM,QAAQ,GAAG,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,4BAA4B,QAAQ,YAAY,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAClF,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,oCAAoC;QACpC,MAAM,aAAa,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC3C,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACvC,MAAM,SAAS,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1E,MAAM,IAAI,KAAK,CACb,sBAAsB,SAAS,IAAI,QAAQ,YAAY,SAAS,GAAG,CAAC,KAAK;YACvE,aAAa,aAAa,CAAC,SAAS,CAAC,IAAI;YACzC,aAAa,WAAW,CAAC,SAAS,CAAC,EAAE,CACxC,CAAC;IACJ,CAAC;AACH,CAAC"}
package/example/main.tsp CHANGED
@@ -43,8 +43,16 @@ model Item {
43
43
 
44
44
  @route("/items")
45
45
  namespace Items {
46
+ @cacheControl("no-cache")
46
47
  @get
47
- op getItem(@query id: string): {
48
+ op list(): {
49
+ @statusCode statusCode: 200;
50
+ @body items: Item[];
51
+ };
52
+
53
+ @get
54
+ @route("/{id}")
55
+ op getItem(@path id: string): {
48
56
  @statusCode status: 200;
49
57
  @body body: Item;
50
58
  };
@@ -64,6 +72,24 @@ namespace Items {
64
72
  };
65
73
  }
66
74
 
75
+ model Article {
76
+ id: string;
77
+ title: string;
78
+ }
79
+
80
+ @route("/articles")
81
+ namespace Articles {
82
+ @etagCache("article-list")
83
+ @cacheControl("public, max-age=3600")
84
+ @get
85
+ op getArticle(@path id: string): {
86
+ @statusCode statusCode: 200;
87
+ @body body: Article;
88
+ } | {
89
+ @statusCode statusCode: 304;
90
+ };
91
+ }
92
+
67
93
  @route("/consuming")
68
94
  namespace Consuming {
69
95
  @rustOwn
@@ -117,6 +117,29 @@ dependencies = [
117
117
  "tracing",
118
118
  ]
119
119
 
120
+ [[package]]
121
+ name = "axum-extra"
122
+ version = "0.10.3"
123
+ source = "registry+https://github.com/rust-lang/crates.io-index"
124
+ checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96"
125
+ dependencies = [
126
+ "axum",
127
+ "axum-core",
128
+ "bytes",
129
+ "futures-util",
130
+ "headers",
131
+ "http",
132
+ "http-body",
133
+ "http-body-util",
134
+ "mime",
135
+ "pin-project-lite",
136
+ "rustversion",
137
+ "serde_core",
138
+ "tower-layer",
139
+ "tower-service",
140
+ "tracing",
141
+ ]
142
+
120
143
  [[package]]
121
144
  name = "base64"
122
145
  version = "0.22.1"
@@ -562,6 +585,30 @@ dependencies = [
562
585
  "hashbrown 0.15.5",
563
586
  ]
564
587
 
588
+ [[package]]
589
+ name = "headers"
590
+ version = "0.4.1"
591
+ source = "registry+https://github.com/rust-lang/crates.io-index"
592
+ checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
593
+ dependencies = [
594
+ "base64",
595
+ "bytes",
596
+ "headers-core",
597
+ "http",
598
+ "httpdate",
599
+ "mime",
600
+ "sha1",
601
+ ]
602
+
603
+ [[package]]
604
+ name = "headers-core"
605
+ version = "0.3.0"
606
+ source = "registry+https://github.com/rust-lang/crates.io-index"
607
+ checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
608
+ dependencies = [
609
+ "http",
610
+ ]
611
+
565
612
  [[package]]
566
613
  name = "heck"
567
614
  version = "0.5.0"
@@ -1032,6 +1079,7 @@ version = "0.1.0"
1032
1079
  dependencies = [
1033
1080
  "async-trait",
1034
1081
  "axum",
1082
+ "axum-extra",
1035
1083
  "chrono",
1036
1084
  "eyre",
1037
1085
  "futures",
@@ -12,6 +12,7 @@ thiserror = "2.0.18"
12
12
  uuid = { version = "1.23.0", features = ["serde", "v4"] }
13
13
  sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "derive"] }
14
14
  axum = { version = "0.8.8", features = ["multipart"] }
15
+ axum-extra = { version = "0.10", features = ["typed-header"] }
15
16
  eyre = "0.6"
16
17
  async-trait = "0.1"
17
18
  tokio = { version = "1.50.0", features = ["full"] }
@@ -9,6 +9,18 @@ use axum::response::IntoResponse;
9
9
  use axum::Json;
10
10
  use eyre::Result;
11
11
 
12
+
13
+ /// Pluggable ETag cache backend.
14
+ /// Implement this for Redis, Memcached, in-memory HashMap, or any store.
15
+ pub trait EtagCache {
16
+ /// Return the stored ETag string for `key`, or `None` if not cached.
17
+ fn get(&self, key: &str) -> Option<String>;
18
+ /// Store `etag` under `key`.
19
+ fn set(&self, key: &str, etag: &str);
20
+ }
21
+
22
+
23
+
12
24
  #[async_trait]
13
25
  pub trait Server: Send + Sync {
14
26
  type Claims: Send + Sync + 'static;
@@ -16,9 +28,13 @@ pub trait Server: Send + Sync {
16
28
 
17
29
  async fn events_accounts_events(&self, account_id: String) -> Result<EventsAccountsEventsResponse>;
18
30
  async fn pets_list(&self, first_query: String, second_query: String) -> Result<PetsListResponse>;
31
+ async fn items_list(&self) -> Result<ItemsListResponse>;
19
32
  async fn items_get_item(&self, id: String) -> Result<ItemsGetItemResponse>;
20
33
  async fn items_create_item(&mut self, body: Item) -> Result<ItemsCreateItemResponse>;
21
34
  async fn items_update_item(&mut self, id: String, body: Item) -> Result<ItemsUpdateItemResponse>;
35
+ async fn articles_get_article<C: EtagCache + Send + Sync>(
36
+ &self, cache: &C, id: String
37
+ ) -> Result<ArticlesGetArticleResponse>;
22
38
  async fn consuming_consume_and_delete(self, id: String) -> Result<ConsumingConsumeAndDeleteResponse>;
23
39
  async fn consuming_upload(&self, account_id: uuid::Uuid, body: Multipart) -> Result<ConsumingUploadResponse>;
24
40
  }
@@ -50,6 +66,20 @@ impl IntoResponse for PetsListResponse {
50
66
  }
51
67
  }
52
68
 
69
+ #[allow(clippy::type_complexity)]
70
+ pub enum ItemsListResponse {
71
+ Ok(Json<Vec<Item>>),
72
+ }
73
+
74
+ impl IntoResponse for ItemsListResponse {
75
+ fn into_response(self) -> axum::response::Response {
76
+ match self {
77
+
78
+ ItemsListResponse::Ok(body) => (StatusCode::OK, body).into_response(),
79
+ }
80
+ }
81
+ }
82
+
53
83
  #[allow(clippy::type_complexity)]
54
84
  pub enum ItemsGetItemResponse {
55
85
  Ok(Json<Item>),
@@ -92,6 +122,22 @@ impl IntoResponse for ItemsUpdateItemResponse {
92
122
  }
93
123
  }
94
124
 
125
+ #[allow(clippy::type_complexity)]
126
+ pub enum ArticlesGetArticleResponse {
127
+ Ok(Json<Article>),
128
+ NotModified,
129
+ }
130
+
131
+ impl IntoResponse for ArticlesGetArticleResponse {
132
+ fn into_response(self) -> axum::response::Response {
133
+ match self {
134
+
135
+ ArticlesGetArticleResponse::Ok(body) => (StatusCode::OK, body).into_response(),
136
+ ArticlesGetArticleResponse::NotModified => StatusCode::NOT_MODIFIED.into_response(),
137
+ }
138
+ }
139
+ }
140
+
95
141
  #[allow(clippy::type_complexity)]
96
142
  pub enum ConsumingConsumeAndDeleteResponse {
97
143
  Ok,
@@ -137,12 +183,6 @@ pub struct PetsListQuery {
137
183
  pub second_query: String
138
184
  }
139
185
 
140
- #[derive(Debug, Clone, serde::Deserialize)]
141
- pub struct ItemsGetItemQuery {
142
- #[serde(rename = "id")]
143
- pub id: String
144
- }
145
-
146
186
  #[derive(Debug, Clone, serde::Deserialize)]
147
187
  pub struct ItemsUpdateItemQuery {
148
188
  #[serde(rename = "id")]
@@ -195,15 +235,41 @@ where
195
235
  }
196
236
  }
197
237
 
238
+ pub async fn items_list_handler<S>(
239
+ axum::extract::State(service): axum::extract::State<S>,
240
+
241
+ ) -> impl axum::response::IntoResponse
242
+ where
243
+ S: Server+ Clone + Send + Sync + 'static,
244
+ S::Claims: Send + Sync + Clone + 'static,
245
+ {
246
+ let result = service.items_list().await;
247
+ match result {
248
+ Ok(response) => {
249
+ let mut res = response.into_response();
250
+ res.headers_mut().insert(
251
+ axum::http::header::CACHE_CONTROL,
252
+ axum::http::HeaderValue::from_static("no-cache"),
253
+ );
254
+ res
255
+ }
256
+ Err(e) => (
257
+ axum::http::StatusCode::INTERNAL_SERVER_ERROR,
258
+ format!("Internal error: {e}"),
259
+ )
260
+ .into_response(),
261
+ }
262
+ }
263
+
198
264
  pub async fn items_get_item_handler<S>(
199
265
  axum::extract::State(service): axum::extract::State<S>,
200
- Query(params): Query<ItemsGetItemQuery>,
266
+ Path(id): Path<String>,
201
267
  ) -> impl axum::response::IntoResponse
202
268
  where
203
269
  S: Server+ Clone + Send + Sync + 'static,
204
270
  S::Claims: Send + Sync + Clone + 'static,
205
271
  {
206
- let result = service.items_get_item(params.id).await;
272
+ let result = service.items_get_item(id).await;
207
273
  match result {
208
274
  Ok(response) => response.into_response(),
209
275
  Err(e) => (
@@ -253,6 +319,48 @@ where
253
319
  }
254
320
  }
255
321
 
322
+ pub async fn articles_get_article_handler<S, C>(
323
+ axum::extract::State(service): axum::extract::State<S>,
324
+ axum::extract::State(cache): axum::extract::State<C>,
325
+ if_none_match: Option<axum_extra::TypedHeader<axum_extra::headers::IfNoneMatch>>,
326
+ Path(id): Path<String>,
327
+ ) -> impl axum::response::IntoResponse
328
+ where
329
+ S: Server + Send + Sync + 'static,
330
+ C: EtagCache + Clone + Send + Sync + 'static,
331
+ {
332
+ // Check If-None-Match against the cache
333
+ let cache_key = "article-list";
334
+ if let (Some(stored_etag), Some(axum_extra::TypedHeader(inm))) = (cache.get(cache_key.as_ref()), if_none_match) {
335
+ // If the client's ETag matches, respond 304 immediately
336
+ if stored_etag == format!("{:?}", inm).trim_matches('"') {
337
+ let mut res = axum::http::StatusCode::NOT_MODIFIED.into_response();
338
+ res.headers_mut().insert(axum::http::header::CACHE_CONTROL, axum::http::HeaderValue::from_static("public, max-age=3600"));
339
+ return res;
340
+ }
341
+ }
342
+ // Forward to business logic
343
+ let result = service.articles_get_article(&cache, id).await;
344
+ match result {
345
+ Ok(response) => {
346
+ let mut res = response.into_response();
347
+ if let Some(stored_etag) = cache.get(cache_key.as_ref()) {
348
+ res.headers_mut().insert(
349
+ axum::http::header::ETAG,
350
+ axum::http::HeaderValue::from_str(&stored_etag).unwrap(),
351
+ );
352
+ }
353
+ res.headers_mut().insert(axum::http::header::CACHE_CONTROL, axum::http::HeaderValue::from_static("public, max-age=3600"));
354
+ res
355
+ }
356
+ Err(e) => (
357
+ axum::http::StatusCode::INTERNAL_SERVER_ERROR,
358
+ format!("Internal error: {e}"),
359
+ )
360
+ .into_response(),
361
+ }
362
+ }
363
+
256
364
  // NOTE: consuming_consume_and_delete takes self and cannot be used with the router pattern.
257
365
  // It consumes the service, so you need to implement your own handler pattern.
258
366
  pub async fn consuming_consume_and_delete_handler<S>(
@@ -294,19 +402,22 @@ where
294
402
  }
295
403
  }
296
404
 
297
- pub fn create_router<S, M>(service: S, middleware: M) -> Router
405
+ pub fn create_router<S, C, M>(service: S, cache: C, middleware: M) -> Router
298
406
  where
299
407
  S: Server + Clone + Send + Sync + 'static,
300
408
  S::Claims: Send + Sync + Clone + 'static,
409
+ C: EtagCache + Clone + Send + Sync + 'static + axum::extract::FromRef<S>,
301
410
  M: FnOnce(Router<S>) -> Router<S> + Clone + Send + Sync + 'static,
302
411
  {
303
- let mut router = Router::new();
412
+ let mut router = Router::new().with_state(cache);
304
413
  let public = Router::new()
305
414
  .route("/events/{accountId}", get(events_accounts_events_handler::<S>))
306
415
  .route("/pets", get(pets_list_handler::<S>))
307
- .route("/items", get(items_get_item_handler::<S>))
416
+ .route("/items", get(items_list_handler::<S>))
417
+ .route("/items/{id}", get(items_get_item_handler::<S>))
308
418
  .route("/items", post(items_create_item_handler::<S>))
309
419
  .route("/items", put(items_update_item_handler::<S>))
420
+ .route("/articles", get(articles_get_article_handler::<S, C>))
310
421
  .route("/consuming", post(consuming_upload_handler::<S>))
311
422
  ;
312
423
  router = router.merge(public);
@@ -16,6 +16,12 @@ pub struct Item {
16
16
  pub value: i32,
17
17
  }
18
18
 
19
+ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
20
+ pub struct Article {
21
+ pub id: String,
22
+ pub title: String,
23
+ }
24
+
19
25
  #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
20
26
  #[serde(untagged)]
21
27
  pub enum MyEvent {