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.
- package/AGENTS.md +82 -80
- package/CHANGELOG.md +17 -0
- package/dist/src/decorators/cache_control.d.ts +6 -0
- package/dist/src/decorators/cache_control.js +9 -0
- package/dist/src/decorators/cache_control.js.map +1 -0
- package/dist/src/decorators/etag_cache.d.ts +6 -0
- package/dist/src/decorators/etag_cache.js +9 -0
- package/dist/src/decorators/etag_cache.js.map +1 -0
- package/dist/src/decorators/index.d.ts +6 -0
- package/dist/src/decorators/index.js +7 -0
- package/dist/src/decorators/index.js.map +1 -0
- package/dist/src/decorators/rust_attr.d.ts +3 -0
- package/dist/src/decorators/rust_attr.js +45 -0
- package/dist/src/decorators/rust_attr.js.map +1 -0
- package/dist/src/decorators/rust_derive.d.ts +3 -0
- package/dist/src/decorators/rust_derive.js +39 -0
- package/dist/src/decorators/rust_derive.js.map +1 -0
- package/dist/src/decorators/rust_impl.d.ts +2 -0
- package/dist/src/decorators/rust_impl.js +19 -0
- package/dist/src/decorators/rust_impl.js.map +1 -0
- package/dist/src/decorators/rust_self.d.ts +3 -0
- package/dist/src/decorators/rust_self.js +35 -0
- package/dist/src/decorators/rust_self.js.map +1 -0
- package/dist/src/emitter.d.ts +2 -11
- package/dist/src/emitter.js +7 -1282
- package/dist/src/emitter.js.map +1 -1
- package/dist/src/formatter/index.d.ts +2 -0
- package/dist/src/formatter/index.js +3 -0
- package/dist/src/formatter/index.js.map +1 -0
- package/dist/src/formatter/mappings.d.ts +4 -0
- package/dist/src/formatter/mappings.js +68 -0
- package/dist/src/formatter/mappings.js.map +1 -0
- package/dist/src/formatter/strings.d.ts +4 -0
- package/dist/src/formatter/strings.js +32 -0
- package/dist/src/formatter/strings.js.map +1 -0
- package/dist/src/generator/etag_router.d.ts +30 -0
- package/dist/src/generator/etag_router.js +123 -0
- package/dist/src/generator/etag_router.js.map +1 -0
- package/dist/src/generator/index.d.ts +5 -0
- package/dist/src/generator/index.js +6 -0
- package/dist/src/generator/index.js.map +1 -0
- package/dist/src/generator/response_enums.d.ts +6 -0
- package/dist/src/generator/response_enums.js +58 -0
- package/dist/src/generator/response_enums.js.map +1 -0
- package/dist/src/generator/router.d.ts +7 -0
- package/dist/src/generator/router.js +227 -0
- package/dist/src/generator/router.js.map +1 -0
- package/dist/src/generator/server_trait.d.ts +6 -0
- package/dist/src/generator/server_trait.js +97 -0
- package/dist/src/generator/server_trait.js.map +1 -0
- package/dist/src/generator/types_file.d.ts +11 -0
- package/dist/src/generator/types_file.js +209 -0
- package/dist/src/generator/types_file.js.map +1 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib.js +1 -1
- package/dist/src/lib.js.map +1 -1
- package/dist/src/models/index.d.ts +2 -0
- package/dist/src/models/index.js +3 -0
- package/dist/src/models/index.js.map +1 -0
- package/dist/src/models/keys.d.ts +6 -0
- package/dist/src/models/keys.js +8 -0
- package/dist/src/models/keys.js.map +1 -0
- package/dist/src/models/types.d.ts +45 -0
- package/dist/src/models/types.js +2 -0
- package/dist/src/models/types.js.map +1 -0
- package/dist/src/parser/decorators.d.ts +18 -0
- package/dist/src/parser/decorators.js +28 -0
- package/dist/src/parser/decorators.js.map +1 -0
- package/dist/src/parser/index.d.ts +6 -0
- package/dist/src/parser/index.js +7 -0
- package/dist/src/parser/index.js.map +1 -0
- package/dist/src/parser/operations.d.ts +13 -0
- package/dist/src/parser/operations.js +127 -0
- package/dist/src/parser/operations.js.map +1 -0
- package/dist/src/parser/parameters.d.ts +5 -0
- package/dist/src/parser/parameters.js +98 -0
- package/dist/src/parser/parameters.js.map +1 -0
- package/dist/src/parser/responses.d.ts +13 -0
- package/dist/src/parser/responses.js +132 -0
- package/dist/src/parser/responses.js.map +1 -0
- package/dist/src/parser/routes.d.ts +4 -0
- package/dist/src/parser/routes.js +36 -0
- package/dist/src/parser/routes.js.map +1 -0
- package/dist/src/parser/types.d.ts +9 -0
- package/dist/src/parser/types.js +157 -0
- package/dist/src/parser/types.js.map +1 -0
- package/dist/test/etag_cache.test.d.ts +1 -0
- package/dist/test/etag_cache.test.js +62 -0
- package/dist/test/etag_cache.test.js.map +1 -0
- package/dist/test/test-host.d.ts +11 -0
- package/dist/test/test-host.js +28 -0
- package/dist/test/test-host.js.map +1 -1
- package/example/main.tsp +27 -1
- package/example/output-rust/Cargo.lock +48 -0
- package/example/output-rust/Cargo.toml +1 -0
- package/example/output-rust/src/generated/server.rs +122 -11
- package/example/output-rust/src/generated/types.rs +6 -0
- package/example/output-rust/src/main.rs +60 -27
- package/justfile +31 -2
- package/package.json +1 -1
- package/scripts/update-golden.js +36 -0
- package/src/decorators/cache_control.ts +14 -0
- package/src/decorators/etag_cache.ts +14 -0
- package/src/decorators/index.ts +6 -0
- package/src/decorators/rust_attr.ts +61 -0
- package/src/decorators/rust_derive.ts +55 -0
- package/src/decorators/rust_impl.ts +29 -0
- package/src/decorators/rust_self.ts +42 -0
- package/src/emitter.ts +18 -1654
- package/src/formatter/index.ts +2 -0
- package/src/formatter/mappings.ts +70 -0
- package/src/formatter/strings.ts +33 -0
- package/src/generator/etag_router.ts +147 -0
- package/src/generator/index.ts +5 -0
- package/src/generator/response_enums.ts +76 -0
- package/src/generator/router.ts +280 -0
- package/src/generator/server_trait.ts +134 -0
- package/src/generator/types_file.ts +297 -0
- package/src/index.ts +3 -1
- package/src/lib.ts +1 -1
- package/src/lib.tsp +3 -1
- package/src/models/index.ts +2 -0
- package/src/models/keys.ts +7 -0
- package/src/models/types.ts +54 -0
- package/src/parser/decorators.ts +34 -0
- package/src/parser/index.ts +6 -0
- package/src/parser/operations.ts +158 -0
- package/src/parser/parameters.ts +117 -0
- package/src/parser/responses.ts +170 -0
- package/src/parser/routes.ts +47 -0
- package/src/parser/types.ts +215 -0
- package/test/etag_cache.test.ts +69 -0
- package/test/golden/etag_cache/server.rs +109 -0
- package/test/golden/etag_cache/spec.tsp +20 -0
- package/test/golden/etag_cache/types.rs +13 -0
- 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":"AAAA,OAAO,EAEL,SAAS,EACT,UAAU,EAEV,gBAAgB,EAChB,iBAAiB,GASlB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAGvD,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACjC,UAAU;IACV,gBAAgB;IAChB,gBAAgB;IAChB,mBAAmB;IACnB,oBAAoB;IACpB,uBAAuB;CACxB,CAAC,CAAC;AAEH,MAAM,UAAU,iBAAiB,CAAC,EAAyB;IACzD,IAAI,CAAC,EAAE;QAAE,OAAO,KAAK,CAAC;IACtB,MAAM,QAAQ,GAAG,EAAE,CAAC,IAAI,CAAC;IACzB,IAAI,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAClD,IAAI,EAAE,CAAC,SAAS;QAAE,OAAO,iBAAiB,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC;IACzD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAU;IACrC,IAAI,WAAW,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,EAAE,GAAI,IAAsC,CAAC,SAAS,CAAC;QAC7D,IAAI,iBAAiB,CAAC,EAAe,CAAC;YAAE,OAAO,IAAI,CAAC;IACtD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,IAAU,EACV,OAAgB,EAChB,cAAwD;IAExD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAc,CAAC;IAEjC,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACrB,MAAM,KAAK,GAAG,IAAa,CAAC;QAC5B,IAAI,gBAAgB,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;YACpD,MAAM,OAAO,GAAG,sBAAsB,CACpC,KAAK,CAAC,OAAO,CAAC,KAAK,EACnB,OAAO,EACP,cAAc,CACf,CAAC;YACF,OAAO,EAAE,IAAI,EAAE,OAAO,OAAO,CAAC,IAAI,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;QAClE,CAAC;QACD,IAAI,iBAAiB,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;YACrD,MAAM,KAAK,GAAG,sBAAsB,CAClC,KAAK,CAAC,OAAO,CAAC,KAAK,EACnB,OAAO,EACP,cAAc,CACf,CAAC;YACF,OAAO;gBACL,IAAI,EAAE,qCAAqC,KAAK,CAAC,IAAI,GAAG;gBACxD,eAAe,EAAE,KAAK;aACvB,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;IACpE,CAAC;IAED,IAAI,IAAI,KAAK,eAAe,EAAE,CAAC;QAC7B,OAAO,sBAAsB,CAC1B,IAAsB,CAAC,IAAI,EAC5B,OAAO,EACP,cAAc,CACf,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO,EAAE,IAAI,EAAE,YAAY,CAAE,IAAa,CAAC,IAAI,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;IAC7E,CAAC;IAED,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACrB,MAAM,SAAS,GAAG,IAAa,CAAC;QAChC,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QACzD,MAAM,iBAAiB,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;QAE1E,IAAI,iBAAiB,EAAE,CAAC;YACtB,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC;gBACnB,OAAO;oBACL,IAAI,EAAE,YAAY,CAAC,SAAS,CAAC,IAAI,CAAC;oBAClC,eAAe,EAAE,KAAK;iBACvB,CAAC;YACJ,CAAC;YACD,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAE,CAAC,CAAC,IAAsB,CAAC,KAAK,CAAC,CAAC;YACpE,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;YACzD,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAClE,MAAM,QAAQ,GAAG,OAAO,QAAQ,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC;YACtD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAClC,cAAc,CAAC,GAAG,CAAC,QAAQ,EAAE;oBAC3B,QAAQ;oBACR,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAqB,CAAC;iBACvD,CAAC,CAAC;YACL,CAAC;YACD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;QACpD,CAAC;QAED,MAAM,eAAe,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;YAC5C,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC;YAClB,IAAK,EAAE,CAAC,IAAe,KAAK,MAAM;gBAAE,OAAO,KAAK,CAAC;YACjD,IACG,EAAE,CAAC,IAAe,KAAK,WAAW;gBAClC,EAAoB,CAAC,IAAI,KAAK,MAAM;gBAErC,OAAO,KAAK,CAAC;YACf,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CAAC;QAEH,IACE,eAAe,CAAC,MAAM,KAAK,CAAC;YAC5B,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;gBACpB,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;oBAClB,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC;oBAClB,OAAO,CACJ,EAAE,CAAC,IAAe,KAAK,MAAM;wBAC9B,CAAE,EAAE,CAAC,IAAe,KAAK,WAAW;4BACjC,EAAoB,CAAC,IAAI,KAAK,MAAM,CAAC,CACzC,CAAC;gBACJ,CAAC,CAAC,CAAC,EACL,CAAC;YACD,MAAM,EAAE,GAAG,sBAAsB,CAC/B,eAAe,CAAC,CAAC,CAAC,CAAC,IAAI,EACvB,OAAO,EACP,cAAc,CACf,CAAC;YACF,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,IAAI,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;QAChE,CAAC;QAED,MAAM,cAAc,GAAa,EAAE,CAAC;QACpC,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;YACzB,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC;YAClB,IAAK,EAAE,CAAC,IAAe,KAAK,MAAM;gBAAE,SAAS;YAC7C,IACG,EAAE,CAAC,IAAe,KAAK,WAAW;gBAClC,EAAoB,CAAC,IAAI,KAAK,MAAM;gBAErC,SAAS;YACX,MAAM,MAAM,GAAG,sBAAsB,CAAC,EAAE,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;YACnE,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC;QACD,MAAM,WAAW,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC;QACjD,MAAM,UAAU,GACd,WAAW,CAAC,MAAM,KAAK,CAAC;YACtB,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;YAChB,CAAC,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;QACrC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;IACtD,CAAC;IAED,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,IAAc,CAAC;QAC9B,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1C,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAE5C,IAAI,MAAM,IAAI,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAC/C,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;QAChE,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;QACrE,CAAC;QACD,OAAO;YACL,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI;YAC9C,eAAe,EAAE,KAAK;SACvB,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;QACzB,MAAM,SAAS,GAAG,IAAqB,CAAC;QACxC,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACxC,IAAI,MAAM,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC;YACnC,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;QAChE,CAAC;QACD,OAAO;YACL,IAAI,EAAE,YAAY,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,mBAAmB;YACzD,eAAe,EAAE,KAAK;SACvB,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;IACpD,CAAC;IAED,IAAI,IAAI,KAAK,eAAe,EAAE,CAAC;QAC7B,OAAO;YACL,IAAI,EAAE,QAAQ;YACd,eAAe,EAAE,IAAI;YACrB,kBAAkB,EAAG,IAAsB,CAAC,KAAK;SAClD,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,gBAAgB,EAAE,CAAC;QACpD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;IAClD,CAAC;IAED,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,gBAAgB,EAAE,CAAC;QACnD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;IACjD,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,mBAAmB,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC;AAC/D,CAAC"}
|
|
@@ -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"}
|
package/dist/test/test-host.d.ts
CHANGED
|
@@ -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;
|
package/dist/test/test-host.js
CHANGED
|
@@ -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;
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 {
|