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
package/src/emitter.ts
CHANGED
|
@@ -3,1664 +3,26 @@ import {
|
|
|
3
3
|
emitFile,
|
|
4
4
|
resolvePath,
|
|
5
5
|
navigateProgram,
|
|
6
|
-
getDoc,
|
|
7
|
-
isArrayModelType,
|
|
8
|
-
isRecordModelType,
|
|
9
|
-
getFormat,
|
|
10
|
-
getPattern,
|
|
11
|
-
Type,
|
|
12
6
|
Model,
|
|
13
|
-
ModelProperty,
|
|
14
7
|
Enum,
|
|
15
8
|
Union,
|
|
16
9
|
Scalar,
|
|
17
|
-
Program,
|
|
18
|
-
Namespace,
|
|
19
|
-
IntrinsicType,
|
|
20
|
-
StringLiteral,
|
|
21
|
-
DecoratorContext,
|
|
22
|
-
getNamespaceFullName,
|
|
23
|
-
Operation,
|
|
24
|
-
getTags,
|
|
25
10
|
} from "@typespec/compiler";
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const rustImplKey = Symbol("rustImpl");
|
|
42
|
-
const rustSelfReceiverKey = Symbol("rustSelfReceiver");
|
|
43
|
-
|
|
44
|
-
type SelfReceiver = "&self" | "&mut self" | "self";
|
|
45
|
-
|
|
46
|
-
interface RustImplInfo {
|
|
47
|
-
impl: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function $rustDerive(
|
|
51
|
-
context: DecoratorContext,
|
|
52
|
-
target: Type,
|
|
53
|
-
derive: string,
|
|
54
|
-
) {
|
|
55
|
-
if (target.kind !== "Model" && target.kind !== "Enum") {
|
|
56
|
-
context.program.reportDiagnostic({
|
|
57
|
-
code: "rust-derive-invalid-target",
|
|
58
|
-
message: `@rustDerive can only be applied to models and enums`,
|
|
59
|
-
severity: "error",
|
|
60
|
-
target: context.decoratorTarget,
|
|
61
|
-
});
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const ns =
|
|
66
|
-
target.kind === "Model"
|
|
67
|
-
? target.namespace
|
|
68
|
-
? getNamespaceFullName(target.namespace)
|
|
69
|
-
: ""
|
|
70
|
-
: target.namespace
|
|
71
|
-
? getNamespaceFullName(target.namespace)
|
|
72
|
-
: "";
|
|
73
|
-
|
|
74
|
-
if (!ns.startsWith("TypeSpec")) {
|
|
75
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
|
-
const info = (target as any)[rustDeriveKey] as RustDeriveInfo | undefined;
|
|
77
|
-
if (info) {
|
|
78
|
-
if (!info.derives.includes(derive)) {
|
|
79
|
-
info.derives.push(derive);
|
|
80
|
-
}
|
|
81
|
-
} else {
|
|
82
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
-
(target as any)[rustDeriveKey] = { derives: [derive] };
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function $rustDerives(
|
|
89
|
-
context: DecoratorContext,
|
|
90
|
-
target: Type,
|
|
91
|
-
...derives: string[]
|
|
92
|
-
) {
|
|
93
|
-
for (const derive of derives) {
|
|
94
|
-
$rustDerive(context, target, derive);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function $rustAttr(
|
|
99
|
-
context: DecoratorContext,
|
|
100
|
-
target: Type,
|
|
101
|
-
attr: string,
|
|
102
|
-
) {
|
|
103
|
-
if (
|
|
104
|
-
target.kind !== "Model" &&
|
|
105
|
-
target.kind !== "Enum" &&
|
|
106
|
-
target.kind !== "ModelProperty"
|
|
107
|
-
) {
|
|
108
|
-
context.program.reportDiagnostic({
|
|
109
|
-
code: "rust-attr-invalid-target",
|
|
110
|
-
message: `@rustAttr can only be applied to models, enums, and model properties`,
|
|
111
|
-
severity: "error",
|
|
112
|
-
target: context.decoratorTarget,
|
|
113
|
-
});
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
let ns: Namespace | undefined;
|
|
118
|
-
if (target.kind === "Model") {
|
|
119
|
-
ns = target.namespace;
|
|
120
|
-
} else if (target.kind === "Enum") {
|
|
121
|
-
ns = target.namespace;
|
|
122
|
-
} else if (target.kind === "ModelProperty") {
|
|
123
|
-
ns = target.model?.namespace;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const nsFullName = ns ? getNamespaceFullName(ns) : "";
|
|
127
|
-
if (!nsFullName.startsWith("TypeSpec")) {
|
|
128
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
-
const info = (target as any)[rustAttrKey] as RustAttrInfo | undefined;
|
|
130
|
-
if (info) {
|
|
131
|
-
if (!info.attrs.includes(attr)) {
|
|
132
|
-
info.attrs.push(attr);
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
-
(target as any)[rustAttrKey] = { attrs: [attr] };
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function $rustAttrs(
|
|
142
|
-
context: DecoratorContext,
|
|
143
|
-
target: Type,
|
|
144
|
-
...attrs: string[]
|
|
145
|
-
) {
|
|
146
|
-
for (const attr of attrs) {
|
|
147
|
-
$rustAttr(context, target, attr);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function $rustImpl(
|
|
152
|
-
context: DecoratorContext,
|
|
153
|
-
target: Type,
|
|
154
|
-
impl: string,
|
|
155
|
-
) {
|
|
156
|
-
if (target.kind !== "Model") {
|
|
157
|
-
context.program.reportDiagnostic({
|
|
158
|
-
code: "rust-impl-invalid-target",
|
|
159
|
-
message: `@rustImpl can only be applied to models`,
|
|
160
|
-
severity: "error",
|
|
161
|
-
target: context.decoratorTarget,
|
|
162
|
-
});
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const ns = target.namespace ? getNamespaceFullName(target.namespace) : "";
|
|
167
|
-
|
|
168
|
-
if (!ns.startsWith("TypeSpec")) {
|
|
169
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
170
|
-
(target as any)[rustImplKey] = { impl: impl };
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function $rustMut(context: DecoratorContext, target: Type) {
|
|
175
|
-
if (target.kind !== "Operation") {
|
|
176
|
-
context.program.reportDiagnostic({
|
|
177
|
-
code: "rust-mut-invalid-target",
|
|
178
|
-
message: `@rustMut can only be applied to operations`,
|
|
179
|
-
severity: "error",
|
|
180
|
-
target: context.decoratorTarget,
|
|
181
|
-
});
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const ns = target.namespace ? getNamespaceFullName(target.namespace) : "";
|
|
186
|
-
if (!ns.startsWith("TypeSpec")) {
|
|
187
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
188
|
-
(target as any)[rustSelfReceiverKey] = "&mut self";
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function $rustOwn(context: DecoratorContext, target: Type) {
|
|
193
|
-
if (target.kind !== "Operation") {
|
|
194
|
-
context.program.reportDiagnostic({
|
|
195
|
-
code: "rust-own-invalid-target",
|
|
196
|
-
message: `@rustOwn can only be applied to operations`,
|
|
197
|
-
severity: "error",
|
|
198
|
-
target: context.decoratorTarget,
|
|
199
|
-
});
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const ns = target.namespace ? getNamespaceFullName(target.namespace) : "";
|
|
204
|
-
if (!ns.startsWith("TypeSpec")) {
|
|
205
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
206
|
-
(target as any)[rustSelfReceiverKey] = "self";
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
|
|
211
|
-
|
|
212
|
-
interface OperationInfo {
|
|
213
|
-
name: string;
|
|
214
|
-
method: HttpMethod;
|
|
215
|
-
path: string;
|
|
216
|
-
tags: string[];
|
|
217
|
-
parameters: ParameterInfo[];
|
|
218
|
-
body: ModelProperty | undefined;
|
|
219
|
-
responses: ResponseInfo[];
|
|
220
|
-
doc: string | undefined;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
interface ParameterInfo {
|
|
224
|
-
name: string;
|
|
225
|
-
rustName: string;
|
|
226
|
-
rustType: string;
|
|
227
|
-
location: "path" | "query" | "header" | "cookie";
|
|
228
|
-
required: boolean;
|
|
229
|
-
optional?: boolean;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
interface ResponseInfo {
|
|
233
|
-
statusCode: number;
|
|
234
|
-
bodyType: string | undefined;
|
|
235
|
-
bodyDescription: string | undefined;
|
|
236
|
-
isSse?: boolean;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function getDecoratorName(decorator: {
|
|
240
|
-
node?: { target?: { sv?: string; kind?: string } };
|
|
241
|
-
}): string {
|
|
242
|
-
if (!decorator) return "";
|
|
243
|
-
if (typeof decorator !== "object") return "";
|
|
244
|
-
|
|
245
|
-
if (decorator.node?.target?.sv) {
|
|
246
|
-
return decorator.node.target.sv;
|
|
247
|
-
}
|
|
248
|
-
if (
|
|
249
|
-
decorator.node?.target?.kind === "Identifier" &&
|
|
250
|
-
decorator.node?.target?.sv
|
|
251
|
-
) {
|
|
252
|
-
return decorator.node.target.sv;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return "";
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function getDecoratorArgValue(
|
|
259
|
-
decorator: {
|
|
260
|
-
args?: { jsValue?: unknown; value?: unknown }[];
|
|
261
|
-
arguments?: { jsValue?: unknown; value?: unknown }[];
|
|
262
|
-
},
|
|
263
|
-
index: number = 0,
|
|
264
|
-
): string {
|
|
265
|
-
if (!decorator) return "";
|
|
266
|
-
const args = decorator.args || decorator.arguments;
|
|
267
|
-
if (!args) return "";
|
|
268
|
-
const arg = args[index];
|
|
269
|
-
if (arg?.jsValue !== undefined) return String(arg.jsValue);
|
|
270
|
-
if (arg?.value !== undefined) return String(arg.value);
|
|
271
|
-
return "";
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function getHttpMethod(
|
|
275
|
-
_program: Program,
|
|
276
|
-
operation: Operation,
|
|
277
|
-
): HttpMethod | undefined {
|
|
278
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
279
|
-
const decorators = (operation as any).decorators;
|
|
280
|
-
if (!decorators) return undefined;
|
|
281
|
-
|
|
282
|
-
for (const key of Object.keys(decorators)) {
|
|
283
|
-
const decorator = decorators[key];
|
|
284
|
-
const name = getDecoratorName(decorator);
|
|
285
|
-
if (name === "get") return "GET";
|
|
286
|
-
if (name === "post") return "POST";
|
|
287
|
-
if (name === "put") return "PUT";
|
|
288
|
-
if (name === "patch") return "PATCH";
|
|
289
|
-
if (name === "delete") return "DELETE";
|
|
290
|
-
if (name === "head") return "HEAD";
|
|
291
|
-
}
|
|
292
|
-
return undefined;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function getRoute(_program: Program, target: Namespace | Operation): string {
|
|
296
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
297
|
-
const decorators = (target as any).decorators;
|
|
298
|
-
if (!decorators) return "";
|
|
299
|
-
|
|
300
|
-
for (const key of Object.keys(decorators)) {
|
|
301
|
-
const decorator = decorators[key];
|
|
302
|
-
const name = getDecoratorName(decorator);
|
|
303
|
-
if (name === "route") {
|
|
304
|
-
return getDecoratorArgValue(decorator, 0);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return "";
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function getFullRoute(program: Program, ns: Namespace): string {
|
|
311
|
-
const routes: string[] = [];
|
|
312
|
-
let current: Namespace | undefined = ns;
|
|
313
|
-
|
|
314
|
-
while (current) {
|
|
315
|
-
const route = getRoute(program, current);
|
|
316
|
-
if (route) {
|
|
317
|
-
routes.unshift(route);
|
|
318
|
-
}
|
|
319
|
-
current = current.namespace;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return routes.join("");
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function hasAuthDecorator(operation: Operation): boolean {
|
|
326
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
327
|
-
const decorators = (operation as any).decorators;
|
|
328
|
-
if (!decorators) return false;
|
|
329
|
-
|
|
330
|
-
for (const key of Object.keys(decorators)) {
|
|
331
|
-
const decorator = decorators[key];
|
|
332
|
-
const name = getDecoratorName(decorator);
|
|
333
|
-
if (name === "useAuth") {
|
|
334
|
-
return true;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
return false;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function getSelfReceiver(operation: Operation): SelfReceiver {
|
|
341
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
342
|
-
const receiver = (operation as any)[rustSelfReceiverKey] as
|
|
343
|
-
| SelfReceiver
|
|
344
|
-
| undefined;
|
|
345
|
-
return receiver ?? "&self";
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function getOperationParameters(
|
|
349
|
-
program: Program,
|
|
350
|
-
operation: Operation,
|
|
351
|
-
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
352
|
-
): ParameterInfo[] {
|
|
353
|
-
const params: ParameterInfo[] = [];
|
|
354
|
-
const model = operation.parameters;
|
|
355
|
-
|
|
356
|
-
for (const [propName, prop] of model.properties) {
|
|
357
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
358
|
-
const decorators = (prop as any).decorators;
|
|
359
|
-
|
|
360
|
-
// Skip body parameters - they are handled separately
|
|
361
|
-
let isBody = false;
|
|
362
|
-
if (decorators) {
|
|
363
|
-
for (const key of Object.keys(decorators)) {
|
|
364
|
-
const decorator = decorators[key];
|
|
365
|
-
const name = getDecoratorName(decorator);
|
|
366
|
-
if (
|
|
367
|
-
name === "body" ||
|
|
368
|
-
name === "bodyRoot" ||
|
|
369
|
-
name === "multipartBody"
|
|
370
|
-
) {
|
|
371
|
-
isBody = true;
|
|
372
|
-
break;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
if (isBody) continue;
|
|
377
|
-
|
|
378
|
-
let location: "path" | "query" | "header" | "cookie" = "query";
|
|
379
|
-
let rustName = toRustIdent(propName);
|
|
380
|
-
|
|
381
|
-
if (decorators) {
|
|
382
|
-
for (const key of Object.keys(decorators)) {
|
|
383
|
-
const decorator = decorators[key];
|
|
384
|
-
const name = getDecoratorName(decorator);
|
|
385
|
-
if (name === "path") {
|
|
386
|
-
location = "path";
|
|
387
|
-
break;
|
|
388
|
-
} else if (name === "query") {
|
|
389
|
-
location = "query";
|
|
390
|
-
break;
|
|
391
|
-
} else if (name === "header") {
|
|
392
|
-
location = "header";
|
|
393
|
-
const headerVal = getDecoratorArgValue(decorator, 0);
|
|
394
|
-
if (headerVal) {
|
|
395
|
-
rustName = toRustIdent(headerVal);
|
|
396
|
-
}
|
|
397
|
-
break;
|
|
398
|
-
} else if (name === "cookie") {
|
|
399
|
-
location = "cookie";
|
|
400
|
-
break;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const { type: rustType } = getRustTypeForProperty(
|
|
406
|
-
prop.type,
|
|
407
|
-
program,
|
|
408
|
-
anonymousEnums,
|
|
409
|
-
);
|
|
410
|
-
|
|
411
|
-
params.push({
|
|
412
|
-
name: propName,
|
|
413
|
-
rustName,
|
|
414
|
-
rustType,
|
|
415
|
-
location,
|
|
416
|
-
required: !prop.optional,
|
|
417
|
-
optional: prop.optional,
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
return params;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function getOperationBody(operation: Operation): ModelProperty | undefined {
|
|
425
|
-
for (const [_propName, prop] of operation.parameters.properties) {
|
|
426
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
427
|
-
const decorators = (prop as any).decorators;
|
|
428
|
-
if (!decorators) continue;
|
|
429
|
-
|
|
430
|
-
for (const key of Object.keys(decorators)) {
|
|
431
|
-
const decorator = decorators[key];
|
|
432
|
-
const name = getDecoratorName(decorator);
|
|
433
|
-
if (name === "body" || name === "bodyRoot" || name === "multipartBody") {
|
|
434
|
-
return prop;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return undefined;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function hasMultipartBody(operation: Operation): boolean {
|
|
442
|
-
for (const [_propName, prop] of operation.parameters.properties) {
|
|
443
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
444
|
-
const decorators = (prop as any).decorators;
|
|
445
|
-
if (!decorators) continue;
|
|
446
|
-
|
|
447
|
-
for (const key of Object.keys(decorators)) {
|
|
448
|
-
const decorator = decorators[key];
|
|
449
|
-
const name = getDecoratorName(decorator);
|
|
450
|
-
if (name === "multipartBody") {
|
|
451
|
-
return true;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return false;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function getOperationResponses(
|
|
459
|
-
program: Program,
|
|
460
|
-
operation: Operation,
|
|
461
|
-
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
462
|
-
): ResponseInfo[] {
|
|
463
|
-
const responses: ResponseInfo[] = [];
|
|
464
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
465
|
-
const returnType = operation.returnType as any;
|
|
466
|
-
|
|
467
|
-
// Check for Array return type first (use runtime check since TS doesn't narrow Array)
|
|
468
|
-
if (
|
|
469
|
-
returnType.kind !== "Union" &&
|
|
470
|
-
returnType.kind !== "Model" &&
|
|
471
|
-
returnType.valueType
|
|
472
|
-
) {
|
|
473
|
-
// This is an Array type
|
|
474
|
-
const { type: rustType } = getRustTypeForProperty(
|
|
475
|
-
returnType.valueType,
|
|
476
|
-
program,
|
|
477
|
-
anonymousEnums,
|
|
478
|
-
);
|
|
479
|
-
responses.push({
|
|
480
|
-
statusCode: 200,
|
|
481
|
-
bodyType: rustType,
|
|
482
|
-
bodyDescription: "",
|
|
483
|
-
});
|
|
484
|
-
return responses;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (returnType.kind === "Union") {
|
|
488
|
-
const union = returnType as Union;
|
|
489
|
-
for (const variant of union.variants.values()) {
|
|
490
|
-
const statusCode = getStatusCode(variant);
|
|
491
|
-
const bodyInfo = getBodyFromResponse(variant, program, anonymousEnums);
|
|
492
|
-
responses.push({
|
|
493
|
-
statusCode,
|
|
494
|
-
bodyType: bodyInfo.type,
|
|
495
|
-
bodyDescription: bodyInfo.description,
|
|
496
|
-
isSse: bodyInfo.isSse,
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
} else if (returnType.kind === "Model") {
|
|
500
|
-
const model = returnType as Model;
|
|
501
|
-
if (model.name === "SSEStream") {
|
|
502
|
-
responses.push({
|
|
503
|
-
statusCode: 200,
|
|
504
|
-
bodyType: "axum::response::Response",
|
|
505
|
-
bodyDescription: "Server-Sent Events stream",
|
|
506
|
-
isSse: true,
|
|
507
|
-
});
|
|
508
|
-
return responses;
|
|
509
|
-
}
|
|
510
|
-
let foundStatusCode = false;
|
|
511
|
-
for (const [propName, prop] of model.properties) {
|
|
512
|
-
if (propName === "body") {
|
|
513
|
-
const { type: rustType } = getRustTypeForProperty(
|
|
514
|
-
prop.type,
|
|
515
|
-
program,
|
|
516
|
-
anonymousEnums,
|
|
517
|
-
);
|
|
518
|
-
responses.push({
|
|
519
|
-
statusCode: 200,
|
|
520
|
-
bodyType: rustType,
|
|
521
|
-
bodyDescription: getDoc(program, prop),
|
|
522
|
-
});
|
|
523
|
-
} else if (propName === "statusCode") {
|
|
524
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
525
|
-
const typeAny = prop.type as any;
|
|
526
|
-
if (typeAny.value !== undefined) {
|
|
527
|
-
const statusCode = typeAny.value as number;
|
|
528
|
-
responses.push({
|
|
529
|
-
statusCode,
|
|
530
|
-
bodyType: undefined,
|
|
531
|
-
bodyDescription: undefined,
|
|
532
|
-
});
|
|
533
|
-
foundStatusCode = true;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
if (!foundStatusCode && responses.length === 0) {
|
|
538
|
-
responses.push({
|
|
539
|
-
statusCode: 200,
|
|
540
|
-
bodyType: undefined,
|
|
541
|
-
bodyDescription: undefined,
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
return responses;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function getStatusCode(variant: { type: Type }): number {
|
|
550
|
-
if (variant.type.kind === "Model") {
|
|
551
|
-
const model = variant.type as Model;
|
|
552
|
-
for (const [propName, prop] of model.properties) {
|
|
553
|
-
if (propName === "statusCode") {
|
|
554
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
555
|
-
const typeAny = prop.type as any;
|
|
556
|
-
if (typeAny.value !== undefined) {
|
|
557
|
-
return typeAny.value as number;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
return 200;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function getBodyFromResponse(
|
|
566
|
-
variant: { type: Type },
|
|
567
|
-
program: Program,
|
|
568
|
-
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
569
|
-
): {
|
|
570
|
-
type: string | undefined;
|
|
571
|
-
description: string | undefined;
|
|
572
|
-
isSse?: boolean;
|
|
573
|
-
} {
|
|
574
|
-
if (variant.type.kind === "Model") {
|
|
575
|
-
const model = variant.type as Model;
|
|
576
|
-
if (model.name === "SSEStream") {
|
|
577
|
-
return {
|
|
578
|
-
type: "axum::response::Response",
|
|
579
|
-
description: "Server-Sent Events stream",
|
|
580
|
-
isSse: true,
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
for (const [_propName, prop] of model.properties) {
|
|
584
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
585
|
-
const decorators = (prop as any).decorators;
|
|
586
|
-
let isBody = false;
|
|
587
|
-
if (decorators) {
|
|
588
|
-
for (const key of Object.keys(decorators)) {
|
|
589
|
-
const decorator = decorators[key];
|
|
590
|
-
const name = getDecoratorName(decorator);
|
|
591
|
-
if (name === "body" || name === "bodyRoot") {
|
|
592
|
-
isBody = true;
|
|
593
|
-
break;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
if (isBody) {
|
|
598
|
-
const { type: rustType } = getRustTypeForProperty(
|
|
599
|
-
prop.type,
|
|
600
|
-
program,
|
|
601
|
-
anonymousEnums,
|
|
602
|
-
);
|
|
603
|
-
return { type: rustType, description: getDoc(program, prop) };
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
return { type: undefined, description: undefined };
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function getAllOperations(
|
|
611
|
-
program: Program,
|
|
612
|
-
): { namespace: Namespace; operations: Operation[] }[] {
|
|
613
|
-
const seenNamespaces = new Set<string>();
|
|
614
|
-
const namespaceOps = new Map<string, { ns: Namespace; ops: Operation[] }>();
|
|
615
|
-
|
|
616
|
-
navigateProgram(program, {
|
|
617
|
-
namespace(ns: Namespace) {
|
|
618
|
-
const route = getRoute(program, ns);
|
|
619
|
-
if (!route) return;
|
|
620
|
-
const fullName = getNamespaceFullName(ns);
|
|
621
|
-
if (!seenNamespaces.has(fullName)) {
|
|
622
|
-
seenNamespaces.add(fullName);
|
|
623
|
-
namespaceOps.set(fullName, { ns, ops: [] });
|
|
624
|
-
}
|
|
625
|
-
},
|
|
626
|
-
operation(op: Operation) {
|
|
627
|
-
const method = getHttpMethod(program, op);
|
|
628
|
-
if (!method) return;
|
|
629
|
-
const ns = op.namespace;
|
|
630
|
-
if (!ns) return;
|
|
631
|
-
const fullName = getNamespaceFullName(ns);
|
|
632
|
-
if (!seenNamespaces.has(fullName)) {
|
|
633
|
-
seenNamespaces.add(fullName);
|
|
634
|
-
namespaceOps.set(fullName, { ns, ops: [] });
|
|
635
|
-
}
|
|
636
|
-
const entry = namespaceOps.get(fullName);
|
|
637
|
-
if (entry) {
|
|
638
|
-
entry.ops.push(op);
|
|
639
|
-
}
|
|
640
|
-
},
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
const result: { namespace: Namespace; operations: Operation[] }[] = [];
|
|
644
|
-
for (const [, entry] of namespaceOps) {
|
|
645
|
-
if (entry.ops.length > 0) {
|
|
646
|
-
result.push({ namespace: entry.ns, operations: entry.ops });
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
return result;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function buildFullPath(namespaceRoute: string, operationRoute: string): string {
|
|
653
|
-
let fullPath = namespaceRoute + operationRoute;
|
|
654
|
-
fullPath = fullPath.replace(/\/+/g, "/");
|
|
655
|
-
if (fullPath.length > 1 && fullPath.endsWith("/")) {
|
|
656
|
-
fullPath = fullPath.slice(0, -1);
|
|
657
|
-
}
|
|
658
|
-
return fullPath;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function emitOperationInfo(
|
|
662
|
-
program: Program,
|
|
663
|
-
op: Operation,
|
|
664
|
-
nsRoute: string,
|
|
665
|
-
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
666
|
-
): OperationInfo | undefined {
|
|
667
|
-
const method = getHttpMethod(program, op);
|
|
668
|
-
if (!method) return undefined;
|
|
669
|
-
|
|
670
|
-
const opRoute = getRoute(program, op);
|
|
671
|
-
const fullPath = buildFullPath(nsRoute, opRoute);
|
|
672
|
-
const tags = getTags(program, op) ?? [];
|
|
673
|
-
const params = getOperationParameters(program, op, anonymousEnums);
|
|
674
|
-
const body = getOperationBody(op);
|
|
675
|
-
const responses = getOperationResponses(program, op, anonymousEnums);
|
|
676
|
-
const doc = getDoc(program, op);
|
|
677
|
-
|
|
678
|
-
const opName = op.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
679
|
-
|
|
680
|
-
return {
|
|
681
|
-
name: opName,
|
|
682
|
-
method,
|
|
683
|
-
path: fullPath,
|
|
684
|
-
tags,
|
|
685
|
-
parameters: params,
|
|
686
|
-
body: body,
|
|
687
|
-
responses,
|
|
688
|
-
doc,
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function getStatusVariantName(statusCode: number): string {
|
|
693
|
-
const statusNames: Record<number, string> = {
|
|
694
|
-
200: "Ok",
|
|
695
|
-
201: "Created",
|
|
696
|
-
202: "Accepted",
|
|
697
|
-
204: "NoContent",
|
|
698
|
-
400: "BadRequest",
|
|
699
|
-
401: "Unauthorized",
|
|
700
|
-
403: "Forbidden",
|
|
701
|
-
404: "NotFound",
|
|
702
|
-
409: "Conflict",
|
|
703
|
-
500: "InternalServerError",
|
|
704
|
-
};
|
|
705
|
-
return statusNames[statusCode] || `Status${statusCode}`;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function getHttpStatusCode(statusCode: number): string {
|
|
709
|
-
const statusCodes: Record<number, string> = {
|
|
710
|
-
200: "StatusCode::OK",
|
|
711
|
-
201: "StatusCode::CREATED",
|
|
712
|
-
202: "StatusCode::ACCEPTED",
|
|
713
|
-
204: "StatusCode::NO_CONTENT",
|
|
714
|
-
400: "StatusCode::BAD_REQUEST",
|
|
715
|
-
401: "StatusCode::UNAUTHORIZED",
|
|
716
|
-
403: "StatusCode::FORBIDDEN",
|
|
717
|
-
404: "StatusCode::NOT_FOUND",
|
|
718
|
-
409: "StatusCode::CONFLICT",
|
|
719
|
-
500: "StatusCode::INTERNAL_SERVER_ERROR",
|
|
720
|
-
};
|
|
721
|
-
return statusCodes[statusCode] || `StatusCode::from_u16(${statusCode})`;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function generateServerTrait(
|
|
725
|
-
program: Program,
|
|
726
|
-
namespaceGroups: { namespace: Namespace; operations: Operation[] }[],
|
|
727
|
-
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
728
|
-
): string {
|
|
729
|
-
const parts: string[] = [];
|
|
730
|
-
|
|
731
|
-
parts.push(`use super::types::*;
|
|
732
|
-
use async_trait::async_trait;
|
|
733
|
-
use axum::extract::Path;
|
|
734
|
-
use axum::Extension;
|
|
735
|
-
use axum::http::StatusCode;
|
|
736
|
-
use axum::response::IntoResponse;
|
|
737
|
-
use axum::Json;
|
|
738
|
-
use eyre::Result;
|
|
739
|
-
|
|
740
|
-
#[async_trait]
|
|
741
|
-
pub trait Server: Send + Sync {
|
|
742
|
-
type Claims: Send + Sync + 'static;
|
|
743
|
-
|
|
744
|
-
`);
|
|
745
|
-
|
|
746
|
-
for (const group of namespaceGroups) {
|
|
747
|
-
const nsName = toPascalCase(
|
|
748
|
-
group.namespace.name.replace(/[^a-zA-Z0-9_]/g, "_"),
|
|
749
|
-
);
|
|
750
|
-
|
|
751
|
-
for (const op of group.operations) {
|
|
752
|
-
const opInfo = emitOperationInfo(program, op, "", anonymousEnums);
|
|
753
|
-
if (!opInfo) continue;
|
|
754
|
-
|
|
755
|
-
if (opInfo.doc) {
|
|
756
|
-
parts.push(` ${formatDoc(opInfo.doc)}`);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
const responseName = `${nsName}${toPascalCase(opInfo.name)}Response`;
|
|
760
|
-
const fnName = toRustIdent(`${nsName}_${opInfo.name}`);
|
|
761
|
-
const isProtected = hasAuthDecorator(op);
|
|
762
|
-
|
|
763
|
-
// Build parameter list for the trait method
|
|
764
|
-
const paramParts: string[] = [];
|
|
765
|
-
|
|
766
|
-
// Add path parameters
|
|
767
|
-
for (const param of opInfo.parameters) {
|
|
768
|
-
if (param.location === "path") {
|
|
769
|
-
paramParts.push(`${param.rustName}: ${param.rustType}`);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// Add query parameters
|
|
774
|
-
for (const param of opInfo.parameters) {
|
|
775
|
-
if (param.location === "query") {
|
|
776
|
-
paramParts.push(`${param.rustName}: ${param.rustType}`);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Add body parameter
|
|
781
|
-
if (opInfo.body) {
|
|
782
|
-
if (hasMultipartBody(op)) {
|
|
783
|
-
paramParts.push(`body: Multipart`);
|
|
784
|
-
} else {
|
|
785
|
-
const bodyType = getRustTypeForProperty(
|
|
786
|
-
opInfo.body.type,
|
|
787
|
-
program,
|
|
788
|
-
anonymousEnums,
|
|
789
|
-
);
|
|
790
|
-
paramParts.push(`body: ${bodyType.type}`);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
const paramsStr = paramParts.join(", ");
|
|
795
|
-
const selfReceiver = getSelfReceiver(op);
|
|
796
|
-
|
|
797
|
-
if (isProtected) {
|
|
798
|
-
if (paramsStr) {
|
|
799
|
-
parts.push(
|
|
800
|
-
` async fn ${fnName}(${selfReceiver}, claims: Self::Claims, ${paramsStr}) -> Result<${responseName}>;`,
|
|
801
|
-
);
|
|
802
|
-
} else {
|
|
803
|
-
parts.push(
|
|
804
|
-
` async fn ${fnName}(${selfReceiver}, claims: Self::Claims) -> Result<${responseName}>;`,
|
|
805
|
-
);
|
|
806
|
-
}
|
|
807
|
-
} else {
|
|
808
|
-
if (paramsStr) {
|
|
809
|
-
parts.push(
|
|
810
|
-
` async fn ${fnName}(${selfReceiver}, ${paramsStr}) -> Result<${responseName}>;`,
|
|
811
|
-
);
|
|
812
|
-
} else {
|
|
813
|
-
parts.push(
|
|
814
|
-
` async fn ${fnName}(${selfReceiver}) -> Result<${responseName}>;`,
|
|
815
|
-
);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
parts.push("}");
|
|
822
|
-
|
|
823
|
-
return parts.join("\n");
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function generateResponseEnums(
|
|
827
|
-
program: Program,
|
|
828
|
-
namespaceGroups: { namespace: Namespace; operations: Operation[] }[],
|
|
829
|
-
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
830
|
-
): string {
|
|
831
|
-
const parts: string[] = [];
|
|
832
|
-
|
|
833
|
-
for (const group of namespaceGroups) {
|
|
834
|
-
const nsName = toPascalCase(
|
|
835
|
-
group.namespace.name.replace(/[^a-zA-Z0-9_]/g, "_"),
|
|
836
|
-
);
|
|
837
|
-
|
|
838
|
-
for (const op of group.operations) {
|
|
839
|
-
const opInfo = emitOperationInfo(program, op, "", anonymousEnums);
|
|
840
|
-
if (!opInfo) continue;
|
|
841
|
-
|
|
842
|
-
if (opInfo.responses.length === 0) continue;
|
|
843
|
-
|
|
844
|
-
const responseName = `${nsName}${toPascalCase(opInfo.name)}Response`;
|
|
845
|
-
|
|
846
|
-
const variants: string[] = [];
|
|
847
|
-
for (const resp of opInfo.responses) {
|
|
848
|
-
const variantName = getStatusVariantName(resp.statusCode);
|
|
849
|
-
if (!resp.bodyType) {
|
|
850
|
-
variants.push(` ${variantName},`);
|
|
851
|
-
} else if (resp.isSse) {
|
|
852
|
-
variants.push(` ${variantName}(${resp.bodyType}),`);
|
|
853
|
-
} else {
|
|
854
|
-
variants.push(` ${variantName}(Json<${resp.bodyType}>),`);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
parts.push(`#[allow(clippy::type_complexity)]
|
|
858
|
-
pub enum ${responseName} {
|
|
859
|
-
${variants.join("\n")}
|
|
860
|
-
}
|
|
861
|
-
`);
|
|
862
|
-
|
|
863
|
-
parts.push(`impl IntoResponse for ${responseName} {
|
|
864
|
-
fn into_response(self) -> axum::response::Response {
|
|
865
|
-
match self {
|
|
866
|
-
`);
|
|
867
|
-
for (const resp of opInfo.responses) {
|
|
868
|
-
const variantName = getStatusVariantName(resp.statusCode);
|
|
869
|
-
const statusCodeStr = getHttpStatusCode(resp.statusCode);
|
|
870
|
-
if (!resp.bodyType) {
|
|
871
|
-
parts.push(
|
|
872
|
-
` ${responseName}::${variantName} => ${statusCodeStr}.into_response(),`,
|
|
873
|
-
);
|
|
874
|
-
} else if (resp.isSse) {
|
|
875
|
-
parts.push(
|
|
876
|
-
` ${responseName}::${variantName}(body) => body.into_response(),`,
|
|
877
|
-
);
|
|
878
|
-
} else {
|
|
879
|
-
parts.push(
|
|
880
|
-
` ${responseName}::${variantName}(body) => (${statusCodeStr}, body).into_response(),`,
|
|
881
|
-
);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
parts.push(` }
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
`);
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
return parts.join("\n");
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function generateRouter(
|
|
895
|
-
program: Program,
|
|
896
|
-
namespaceGroups: { namespace: Namespace; operations: Operation[] }[],
|
|
897
|
-
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
898
|
-
): string {
|
|
899
|
-
const handlers: string[] = [];
|
|
900
|
-
const queryTypeStructs: string[] = [];
|
|
901
|
-
const publicRoutes: string[] = [];
|
|
902
|
-
const protectedRoutes: string[] = [];
|
|
903
|
-
const usedMethods = new Set<string>();
|
|
904
|
-
|
|
905
|
-
for (const group of namespaceGroups) {
|
|
906
|
-
const nsRoute = getFullRoute(program, group.namespace);
|
|
907
|
-
if (!nsRoute) continue;
|
|
908
|
-
|
|
909
|
-
const nsName = toPascalCase(
|
|
910
|
-
group.namespace.name.replace(/[^a-zA-Z0-9_]/g, "_"),
|
|
911
|
-
);
|
|
912
|
-
|
|
913
|
-
for (const op of group.operations) {
|
|
914
|
-
const opInfo = emitOperationInfo(program, op, nsRoute, anonymousEnums);
|
|
915
|
-
if (!opInfo) continue;
|
|
916
|
-
|
|
917
|
-
const method = opInfo.method.toLowerCase();
|
|
918
|
-
usedMethods.add(method);
|
|
919
|
-
|
|
920
|
-
const handlerFnName = toRustIdent(`${nsName}_${opInfo.name}`);
|
|
921
|
-
const traitFnName = handlerFnName;
|
|
922
|
-
const isProtected = hasAuthDecorator(op);
|
|
923
|
-
const selfReceiver = getSelfReceiver(op);
|
|
924
|
-
|
|
925
|
-
const pathParams = opInfo.parameters.filter((p) => p.location === "path");
|
|
926
|
-
const hasPathParams = pathParams.length > 0;
|
|
927
|
-
const queryParams = opInfo.parameters.filter(
|
|
928
|
-
(p) => p.location === "query",
|
|
929
|
-
);
|
|
930
|
-
const hasQueryParams = queryParams.length > 0;
|
|
931
|
-
const hasBody = !!opInfo.body;
|
|
932
|
-
const isMultipartBody = hasMultipartBody(op);
|
|
933
|
-
|
|
934
|
-
// Build extractor lines and server method call arguments
|
|
935
|
-
// IMPORTANT: axum requires specific extractor order:
|
|
936
|
-
// State -> Extension -> Path -> Query -> Json/Multipart -> Body
|
|
937
|
-
const extractorLines: string[] = [];
|
|
938
|
-
const serverArgs: string[] = [];
|
|
939
|
-
|
|
940
|
-
// State is always first (added in handler template)
|
|
941
|
-
const serviceBinding =
|
|
942
|
-
selfReceiver === "&mut self" ? "mut service" : "service";
|
|
943
|
-
|
|
944
|
-
// Extension (claims) comes after State
|
|
945
|
-
if (isProtected) {
|
|
946
|
-
extractorLines.push(` Extension(claims): Extension<S::Claims>,`);
|
|
947
|
-
serverArgs.push(`claims`);
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// Path params come after Extension
|
|
951
|
-
if (hasPathParams) {
|
|
952
|
-
const pathTypes = pathParams.map((p) => p.rustType).join(", ");
|
|
953
|
-
const pathFields = pathParams.map((p) => p.rustName).join(", ");
|
|
954
|
-
if (pathParams.length === 1) {
|
|
955
|
-
extractorLines.push(` Path(${pathFields}): Path<${pathTypes}>,`);
|
|
956
|
-
} else {
|
|
957
|
-
extractorLines.push(
|
|
958
|
-
` Path((${pathFields})): Path<(${pathTypes})>,`,
|
|
959
|
-
);
|
|
960
|
-
}
|
|
961
|
-
// Add path params to server method args
|
|
962
|
-
for (const param of pathParams) {
|
|
963
|
-
serverArgs.push(param.rustName);
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// Query params come after Path
|
|
968
|
-
if (hasQueryParams && queryParams.length > 0) {
|
|
969
|
-
const queryTypeName = `${toPascalCase(handlerFnName)}Query`;
|
|
970
|
-
const queryFields = queryParams
|
|
971
|
-
.map(
|
|
972
|
-
(p) =>
|
|
973
|
-
` #[serde(rename = "${p.name}")]\n pub ${p.rustName}: ${p.rustType}`,
|
|
974
|
-
)
|
|
975
|
-
.join(",\n");
|
|
976
|
-
queryTypeStructs.push(
|
|
977
|
-
`#[derive(Debug, Clone, serde::Deserialize)]\npub struct ${queryTypeName} {\n${queryFields}\n}`,
|
|
978
|
-
);
|
|
979
|
-
extractorLines.push(` Query(params): Query<${queryTypeName}>,`);
|
|
980
|
-
for (const param of queryParams) {
|
|
981
|
-
serverArgs.push(`params.${param.rustName}`);
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
// Body comes last (Json or Multipart based on decorator)
|
|
986
|
-
if (hasBody && opInfo.body) {
|
|
987
|
-
if (isMultipartBody) {
|
|
988
|
-
extractorLines.push(` multipart: axum::extract::Multipart,`);
|
|
989
|
-
serverArgs.push(`multipart`);
|
|
990
|
-
} else {
|
|
991
|
-
const bodyType = getRustTypeForProperty(
|
|
992
|
-
opInfo.body.type,
|
|
993
|
-
program,
|
|
994
|
-
anonymousEnums,
|
|
995
|
-
);
|
|
996
|
-
extractorLines.push(` Json(payload): Json<${bodyType.type}>,`);
|
|
997
|
-
serverArgs.push(`payload`);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// Build server method call
|
|
1002
|
-
const serverArgsStr = serverArgs.join(", ");
|
|
1003
|
-
const serverCall = `service.${traitFnName}(${serverArgsStr}).await`;
|
|
1004
|
-
|
|
1005
|
-
// All handlers use <S> generics, Claims is now an associated type
|
|
1006
|
-
// For &mut self, we need Clone because service is extracted multiple times
|
|
1007
|
-
// For self, we can't use Clone (would need Arc/Mutex or different pattern)
|
|
1008
|
-
const needsClone = selfReceiver !== "self" ? "+ Clone" : "";
|
|
1009
|
-
const handlerCode =
|
|
1010
|
-
selfReceiver === "self"
|
|
1011
|
-
? `// NOTE: ${handlerFnName} takes self and cannot be used with the router pattern.
|
|
1012
|
-
// It consumes the service, so you need to implement your own handler pattern.
|
|
1013
|
-
pub async fn ${handlerFnName}_handler<S>(
|
|
1014
|
-
axum::extract::State(service): axum::extract::State<S>,
|
|
1015
|
-
${extractorLines.join("\n")}
|
|
1016
|
-
) -> impl axum::response::IntoResponse
|
|
1017
|
-
where
|
|
1018
|
-
S: Server + Send + Sync + 'static,
|
|
1019
|
-
S::Claims: Send + Sync + Clone + 'static,
|
|
1020
|
-
{
|
|
1021
|
-
let result = service.${traitFnName}(${serverArgsStr}).await;
|
|
1022
|
-
match result {
|
|
1023
|
-
Ok(response) => response.into_response(),
|
|
1024
|
-
Err(e) => (
|
|
1025
|
-
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
|
1026
|
-
format!("Internal error: {e}"),
|
|
1027
|
-
)
|
|
1028
|
-
.into_response(),
|
|
1029
|
-
}
|
|
1030
|
-
}`
|
|
1031
|
-
: `pub async fn ${handlerFnName}_handler<S>(
|
|
1032
|
-
axum::extract::State(${serviceBinding}): axum::extract::State<S>,
|
|
1033
|
-
${extractorLines.join("\n")}
|
|
1034
|
-
) -> impl axum::response::IntoResponse
|
|
1035
|
-
where
|
|
1036
|
-
S: Server${needsClone} + Send + Sync + 'static,
|
|
1037
|
-
S::Claims: Send + Sync + Clone + 'static,
|
|
1038
|
-
{
|
|
1039
|
-
let result = ${serverCall};
|
|
1040
|
-
match result {
|
|
1041
|
-
Ok(response) => response.into_response(),
|
|
1042
|
-
Err(e) => (
|
|
1043
|
-
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
|
1044
|
-
format!("Internal error: {e}"),
|
|
1045
|
-
)
|
|
1046
|
-
.into_response(),
|
|
1047
|
-
}
|
|
1048
|
-
}`;
|
|
1049
|
-
|
|
1050
|
-
handlers.push(handlerCode);
|
|
1051
|
-
|
|
1052
|
-
// Don't add routes for self methods (they consume the service)
|
|
1053
|
-
if (selfReceiver === "self") {
|
|
1054
|
-
continue;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
const routePath = `"${opInfo.path}"`;
|
|
1058
|
-
let routeStmt = "";
|
|
1059
|
-
if (isProtected) {
|
|
1060
|
-
routeStmt = `.route(${routePath}, ${method}(${handlerFnName}_handler::<S>))`;
|
|
1061
|
-
protectedRoutes.push(routeStmt);
|
|
1062
|
-
} else {
|
|
1063
|
-
routeStmt = `.route(${routePath}, ${method}(${handlerFnName}_handler::<S>))`;
|
|
1064
|
-
publicRoutes.push(routeStmt);
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
const methodImports = Array.from(usedMethods).sort().join(", ");
|
|
1070
|
-
|
|
1071
|
-
const routerBody = buildRouterBody(publicRoutes, protectedRoutes);
|
|
1072
|
-
|
|
1073
|
-
const parts: string[] = [];
|
|
1074
|
-
parts.push(`use axum::extract::{Query, Multipart};
|
|
1075
|
-
use axum::routing::{${methodImports}};
|
|
1076
|
-
use axum::Router;
|
|
1077
|
-
|
|
1078
|
-
`);
|
|
1079
|
-
if (queryTypeStructs.length > 0) {
|
|
1080
|
-
parts.push(queryTypeStructs.join("\n\n"));
|
|
1081
|
-
parts.push("\n\n");
|
|
1082
|
-
}
|
|
1083
|
-
parts.push(handlers.join("\n\n"));
|
|
1084
|
-
parts.push(`
|
|
1085
|
-
pub fn create_router<S, M>(service: S, middleware: M) -> Router
|
|
1086
|
-
where
|
|
1087
|
-
S: Server + Clone + Send + Sync + 'static,
|
|
1088
|
-
S::Claims: Send + Sync + Clone + 'static,
|
|
1089
|
-
M: FnOnce(Router<S>) -> Router<S> + Clone + Send + Sync + 'static,
|
|
1090
|
-
{
|
|
1091
|
-
${routerBody}
|
|
1092
|
-
}`);
|
|
1093
|
-
|
|
1094
|
-
return parts.join("\n");
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
function buildRouterBody(
|
|
1098
|
-
publicRoutes: string[],
|
|
1099
|
-
protectedRoutes: string[],
|
|
1100
|
-
): string {
|
|
1101
|
-
const lines: string[] = [];
|
|
1102
|
-
lines.push(" let mut router = Router::new();");
|
|
1103
|
-
if (publicRoutes.length > 0) {
|
|
1104
|
-
lines.push(" let public = Router::new()");
|
|
1105
|
-
for (const r of publicRoutes) {
|
|
1106
|
-
lines.push(` ${r.trim()}`);
|
|
1107
|
-
}
|
|
1108
|
-
lines.push(" ;");
|
|
1109
|
-
lines.push(" router = router.merge(public);");
|
|
1110
|
-
}
|
|
1111
|
-
if (protectedRoutes.length > 0) {
|
|
1112
|
-
lines.push(" let protected = Router::new()");
|
|
1113
|
-
for (const r of protectedRoutes) {
|
|
1114
|
-
lines.push(` ${r.trim()}`);
|
|
1115
|
-
}
|
|
1116
|
-
lines.push(" ;");
|
|
1117
|
-
lines.push(" router = router.merge(middleware(protected));");
|
|
1118
|
-
}
|
|
1119
|
-
lines.push(" router.with_state(service)");
|
|
1120
|
-
return lines.join("\n");
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
const scalarToRust: Record<string, string> = {
|
|
1124
|
-
string: "String",
|
|
1125
|
-
int8: "i8",
|
|
1126
|
-
int16: "i16",
|
|
1127
|
-
int32: "i32",
|
|
1128
|
-
int64: "i64",
|
|
1129
|
-
uint8: "u8",
|
|
1130
|
-
uint16: "u16",
|
|
1131
|
-
uint32: "u32",
|
|
1132
|
-
uint64: "u64",
|
|
1133
|
-
float32: "f32",
|
|
1134
|
-
float64: "f64",
|
|
1135
|
-
boolean: "bool",
|
|
1136
|
-
bytes: "Vec<u8>",
|
|
1137
|
-
plainDate: "chrono::NaiveDate",
|
|
1138
|
-
plainTime: "chrono::NaiveTime",
|
|
1139
|
-
utcDateTime: "chrono::DateTime<chrono::Utc>",
|
|
1140
|
-
offsetDateTime: "chrono::DateTime<chrono::FixedOffset>",
|
|
1141
|
-
plainDateTime: "chrono::NaiveDateTime",
|
|
1142
|
-
duration: "String",
|
|
1143
|
-
numeric: "f64",
|
|
1144
|
-
integer: "i64",
|
|
1145
|
-
float: "f64",
|
|
1146
|
-
safeint: "i64",
|
|
1147
|
-
decimal: "f64",
|
|
1148
|
-
decimal128: "f64",
|
|
1149
|
-
url: "String",
|
|
1150
|
-
};
|
|
1151
|
-
|
|
1152
|
-
const formatToRust: Record<string, string> = {
|
|
1153
|
-
uuid: "uuid::Uuid",
|
|
1154
|
-
date: "chrono::NaiveDate",
|
|
1155
|
-
time: "chrono::NaiveTime",
|
|
1156
|
-
dateTime: "chrono::DateTime<chrono::Utc>",
|
|
1157
|
-
"date-time": "chrono::DateTime<chrono::Utc>",
|
|
1158
|
-
};
|
|
1159
|
-
|
|
1160
|
-
const typeSpecNamespaces = new Set([
|
|
1161
|
-
"TypeSpec",
|
|
1162
|
-
"@typespec/http",
|
|
1163
|
-
"@typespec/rest",
|
|
1164
|
-
"@typespec/openapi",
|
|
1165
|
-
"@typespec/openapi3",
|
|
1166
|
-
"@typespec/json-schema",
|
|
1167
|
-
]);
|
|
1168
|
-
|
|
1169
|
-
function isStdLibNamespace(ns: Namespace | undefined): boolean {
|
|
1170
|
-
if (!ns) return false;
|
|
1171
|
-
const fullName = ns.name;
|
|
1172
|
-
if (typeSpecNamespaces.has(fullName)) return true;
|
|
1173
|
-
if (ns.namespace) return isStdLibNamespace(ns.namespace);
|
|
1174
|
-
return false;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
function isStdLibType(type: Type): boolean {
|
|
1178
|
-
if ("namespace" in type) {
|
|
1179
|
-
const ns = (type as Model | Enum | Union | Scalar).namespace;
|
|
1180
|
-
if (isStdLibNamespace(ns as Namespace)) return true;
|
|
1181
|
-
}
|
|
1182
|
-
return false;
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
interface AnonymousStringLiteralUnion {
|
|
1186
|
-
enumName: string;
|
|
1187
|
-
variants: StringLiteral[];
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
function getRustTypeForProperty(
|
|
1191
|
-
type: Type,
|
|
1192
|
-
program: Program,
|
|
1193
|
-
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
1194
|
-
): { type: string; isStringLiteral: boolean; stringLiteralValue?: string } {
|
|
1195
|
-
const kind = type.kind as string;
|
|
1196
|
-
|
|
1197
|
-
if (kind === "Model") {
|
|
1198
|
-
const model = type as Model;
|
|
1199
|
-
if (isArrayModelType(model) && model.indexer?.value) {
|
|
1200
|
-
const element = getRustTypeForProperty(
|
|
1201
|
-
model.indexer.value,
|
|
1202
|
-
program,
|
|
1203
|
-
anonymousEnums,
|
|
1204
|
-
);
|
|
1205
|
-
return { type: `Vec<${element.type}>`, isStringLiteral: false };
|
|
1206
|
-
}
|
|
1207
|
-
if (isRecordModelType(model) && model.indexer?.value) {
|
|
1208
|
-
const value = getRustTypeForProperty(
|
|
1209
|
-
model.indexer.value,
|
|
1210
|
-
program,
|
|
1211
|
-
anonymousEnums,
|
|
1212
|
-
);
|
|
1213
|
-
return {
|
|
1214
|
-
type: `std::collections::HashMap<String, ${value.type}>`,
|
|
1215
|
-
isStringLiteral: false,
|
|
1216
|
-
};
|
|
1217
|
-
}
|
|
1218
|
-
return { type: toPascalCase(model.name), isStringLiteral: false };
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
if (kind === "ModelProperty") {
|
|
1222
|
-
return getRustTypeForProperty(
|
|
1223
|
-
(type as ModelProperty).type,
|
|
1224
|
-
program,
|
|
1225
|
-
anonymousEnums,
|
|
1226
|
-
);
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
if (kind === "Enum") {
|
|
1230
|
-
return { type: toPascalCase((type as Enum).name), isStringLiteral: false };
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
if (kind === "Union") {
|
|
1234
|
-
const unionType = type as Union;
|
|
1235
|
-
const variants = Array.from(unionType.variants.values());
|
|
1236
|
-
const allStringLiterals = variants.every((v) => v.type.kind === "String");
|
|
1237
|
-
|
|
1238
|
-
if (allStringLiterals) {
|
|
1239
|
-
if (unionType.name) {
|
|
1240
|
-
return {
|
|
1241
|
-
type: toPascalCase(unionType.name),
|
|
1242
|
-
isStringLiteral: false,
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
const values = variants.map((v) => (v.type as StringLiteral).value);
|
|
1246
|
-
const sanitized = values.map((v) => v.replace(/_/g, ""));
|
|
1247
|
-
const firstTwo = sanitized.slice(0, 2).map(toPascalCase).join("");
|
|
1248
|
-
const enumName = `Enum${firstTwo}${sanitized.length}`;
|
|
1249
|
-
if (!anonymousEnums.has(enumName)) {
|
|
1250
|
-
anonymousEnums.set(enumName, {
|
|
1251
|
-
enumName,
|
|
1252
|
-
variants: variants.map((v) => v.type as StringLiteral),
|
|
1253
|
-
});
|
|
1254
|
-
}
|
|
1255
|
-
return { type: enumName, isStringLiteral: false };
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
const nonNullVariants = variants.filter((v) => {
|
|
1259
|
-
const vt = v.type;
|
|
1260
|
-
if ((vt.kind as string) === "Null") return false;
|
|
1261
|
-
if (
|
|
1262
|
-
(vt.kind as string) === "Intrinsic" &&
|
|
1263
|
-
(vt as IntrinsicType).name === "null"
|
|
1264
|
-
)
|
|
1265
|
-
return false;
|
|
1266
|
-
return true;
|
|
1267
|
-
});
|
|
1268
|
-
|
|
1269
|
-
if (
|
|
1270
|
-
nonNullVariants.length === 1 &&
|
|
1271
|
-
(variants.length === 2 ||
|
|
1272
|
-
variants.some((v) => {
|
|
1273
|
-
const vt = v.type;
|
|
1274
|
-
return (
|
|
1275
|
-
(vt.kind as string) === "Null" ||
|
|
1276
|
-
((vt.kind as string) === "Intrinsic" &&
|
|
1277
|
-
(vt as IntrinsicType).name === "null")
|
|
1278
|
-
);
|
|
1279
|
-
}))
|
|
1280
|
-
) {
|
|
1281
|
-
const vt = getRustTypeForProperty(
|
|
1282
|
-
nonNullVariants[0].type,
|
|
1283
|
-
program,
|
|
1284
|
-
anonymousEnums,
|
|
1285
|
-
);
|
|
1286
|
-
return { type: `Option<${vt.type}>`, isStringLiteral: false };
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
const variantStrings: string[] = [];
|
|
1290
|
-
for (const v of variants) {
|
|
1291
|
-
const vt = v.type;
|
|
1292
|
-
if ((vt.kind as string) === "Null") continue;
|
|
1293
|
-
if (
|
|
1294
|
-
(vt.kind as string) === "Intrinsic" &&
|
|
1295
|
-
(vt as IntrinsicType).name === "null"
|
|
1296
|
-
)
|
|
1297
|
-
continue;
|
|
1298
|
-
const rustVt = getRustTypeForProperty(vt, program, anonymousEnums);
|
|
1299
|
-
variantStrings.push(rustVt.type);
|
|
1300
|
-
}
|
|
1301
|
-
const uniqueTypes = [...new Set(variantStrings)];
|
|
1302
|
-
const resultType =
|
|
1303
|
-
uniqueTypes.length === 1
|
|
1304
|
-
? uniqueTypes[0]
|
|
1305
|
-
: `(${uniqueTypes.join(" | ")})`;
|
|
1306
|
-
return { type: resultType, isStringLiteral: false };
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
if (kind === "Scalar") {
|
|
1310
|
-
const scalar = type as Scalar;
|
|
1311
|
-
const format = getFormat(program, scalar);
|
|
1312
|
-
const pattern = getPattern(program, scalar);
|
|
1313
|
-
|
|
1314
|
-
if (format && formatToRust[format] && !pattern) {
|
|
1315
|
-
return { type: formatToRust[format], isStringLiteral: false };
|
|
1316
|
-
}
|
|
1317
|
-
if (pattern) {
|
|
1318
|
-
return { type: toPascalCase(scalar.name), isStringLiteral: false };
|
|
1319
|
-
}
|
|
1320
|
-
return {
|
|
1321
|
-
type: scalarToRust[scalar.name] ?? scalar.name,
|
|
1322
|
-
isStringLiteral: false,
|
|
1323
|
-
};
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
if (kind === "Intrinsic") {
|
|
1327
|
-
const intrinsic = type as IntrinsicType;
|
|
1328
|
-
const format = getFormat(program, type);
|
|
1329
|
-
if (format && formatToRust[format]) {
|
|
1330
|
-
return { type: formatToRust[format], isStringLiteral: false };
|
|
1331
|
-
}
|
|
1332
|
-
return {
|
|
1333
|
-
type: scalarToRust[intrinsic.name] ?? "serde_json::Value",
|
|
1334
|
-
isStringLiteral: false,
|
|
1335
|
-
};
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
if (kind === "String") {
|
|
1339
|
-
return { type: "String", isStringLiteral: false };
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
if (kind === "StringLiteral") {
|
|
1343
|
-
return {
|
|
1344
|
-
type: "String",
|
|
1345
|
-
isStringLiteral: true,
|
|
1346
|
-
stringLiteralValue: (type as StringLiteral).value,
|
|
1347
|
-
};
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
if (kind === "Boolean" || kind === "BooleanLiteral") {
|
|
1351
|
-
return { type: "bool", isStringLiteral: false };
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
if (kind === "Number" || kind === "NumericLiteral") {
|
|
1355
|
-
return { type: "f64", isStringLiteral: false };
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
return { type: "serde_json::Value", isStringLiteral: false };
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
function toRustIdent(name: string): string {
|
|
1362
|
-
const snakeCase = name.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
|
1363
|
-
const result = snakeCase.replace(/[^a-z0-9_]/g, "_");
|
|
1364
|
-
if (/^[0-9]/.test(result)) {
|
|
1365
|
-
return "_" + result;
|
|
1366
|
-
}
|
|
1367
|
-
return result;
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
function toPascalCase(name: string): string {
|
|
1371
|
-
if (/^[^a-z]+$/.test(name)) {
|
|
1372
|
-
return name;
|
|
1373
|
-
}
|
|
1374
|
-
if (/[-_]/.test(name)) {
|
|
1375
|
-
return name
|
|
1376
|
-
.split(/[-_]/)
|
|
1377
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
1378
|
-
.join("");
|
|
1379
|
-
}
|
|
1380
|
-
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
function toRustVariantName(name: string): string {
|
|
1384
|
-
return toPascalCase(name);
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
function formatDoc(doc: string | undefined): string {
|
|
1388
|
-
if (!doc) return "";
|
|
1389
|
-
return doc
|
|
1390
|
-
.split("\n")
|
|
1391
|
-
.map((line) => `/// ${line.trim()}`)
|
|
1392
|
-
.join("\n");
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
function emitStringLiteralUnion(union: Union): string {
|
|
1396
|
-
const parts: string[] = [];
|
|
1397
|
-
const name = toPascalCase(union.name ?? "Value");
|
|
1398
|
-
const variants = Array.from(union.variants.values());
|
|
1399
|
-
|
|
1400
|
-
parts.push(
|
|
1401
|
-
`#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]\n#[allow(clippy::enum_variant_names)]\npub enum ${name} {`,
|
|
1402
|
-
);
|
|
1403
|
-
|
|
1404
|
-
for (let i = 0; i < variants.length; i++) {
|
|
1405
|
-
const literalType = variants[i].type as StringLiteral;
|
|
1406
|
-
const variantName = toPascalCase(literalType.value);
|
|
1407
|
-
const serdeValue = literalType.value;
|
|
1408
|
-
if (i === 0) {
|
|
1409
|
-
parts.push(` #[default]`);
|
|
1410
|
-
}
|
|
1411
|
-
parts.push(` #[serde(rename = "${serdeValue}")]`);
|
|
1412
|
-
parts.push(` ${variantName},`);
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
parts.push("}");
|
|
1416
|
-
return parts.join("\n");
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
function emitModel(
|
|
1420
|
-
model: Model,
|
|
1421
|
-
program: Program,
|
|
1422
|
-
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
1423
|
-
): string {
|
|
1424
|
-
const parts: string[] = [];
|
|
1425
|
-
const name = toPascalCase(model.name);
|
|
1426
|
-
const allProps = getAllProperties(model, program);
|
|
1427
|
-
|
|
1428
|
-
const deriveAttrs = [
|
|
1429
|
-
"Debug",
|
|
1430
|
-
"Clone",
|
|
1431
|
-
"serde::Serialize",
|
|
1432
|
-
"serde::Deserialize",
|
|
1433
|
-
];
|
|
1434
|
-
|
|
1435
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1436
|
-
const customDerives = (model as any)[rustDeriveKey] as
|
|
1437
|
-
| RustDeriveInfo
|
|
1438
|
-
| undefined;
|
|
1439
|
-
if (customDerives) {
|
|
1440
|
-
deriveAttrs.push(...customDerives.derives);
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
parts.push(`#[derive(${deriveAttrs.join(", ")})]`);
|
|
1444
|
-
|
|
1445
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1446
|
-
const customAttrs = (model as any)[rustAttrKey] as RustAttrInfo | undefined;
|
|
1447
|
-
if (customAttrs) {
|
|
1448
|
-
for (const attr of customAttrs.attrs) {
|
|
1449
|
-
parts.push(`#[${attr}]`);
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
if (allProps.size > 0) {
|
|
1454
|
-
const fields: string[] = [];
|
|
1455
|
-
for (const [propName, prop] of allProps) {
|
|
1456
|
-
const doc = getDoc(program, prop);
|
|
1457
|
-
const { type: rustType } = getRustTypeForProperty(
|
|
1458
|
-
prop.type,
|
|
1459
|
-
program,
|
|
1460
|
-
anonymousEnums,
|
|
1461
|
-
);
|
|
1462
|
-
const optional = prop.optional ? true : false;
|
|
1463
|
-
const fieldName = toRustIdent(propName);
|
|
1464
|
-
const serdeRename =
|
|
1465
|
-
propName !== fieldName ? `#[serde(rename = "${propName}")]` : "";
|
|
1466
|
-
|
|
1467
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1468
|
-
const propAttrs = (prop as any)[rustAttrKey] as RustAttrInfo | undefined;
|
|
1469
|
-
|
|
1470
|
-
if (doc) {
|
|
1471
|
-
fields.push(` ${formatDoc(doc)}`);
|
|
1472
|
-
}
|
|
1473
|
-
if (propAttrs) {
|
|
1474
|
-
for (const attr of propAttrs.attrs) {
|
|
1475
|
-
fields.push(` #[${attr}]`);
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
if (serdeRename) {
|
|
1479
|
-
fields.push(` ${serdeRename}`);
|
|
1480
|
-
}
|
|
1481
|
-
if (optional) {
|
|
1482
|
-
fields.push(` #[serde(skip_serializing_if = "Option::is_none")]`);
|
|
1483
|
-
}
|
|
1484
|
-
fields.push(
|
|
1485
|
-
` pub ${fieldName}: ${optional ? `Option<${rustType}>` : rustType},`,
|
|
1486
|
-
);
|
|
1487
|
-
}
|
|
1488
|
-
parts.push(`pub struct ${name} {
|
|
1489
|
-
${fields.join("\n")}
|
|
1490
|
-
}`);
|
|
1491
|
-
} else {
|
|
1492
|
-
parts.push(`pub struct ${name}`);
|
|
1493
|
-
parts.push("(());");
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1497
|
-
const customImpl = (model as any)[rustImplKey] as RustImplInfo | undefined;
|
|
1498
|
-
if (customImpl) {
|
|
1499
|
-
parts.push(`
|
|
1500
|
-
${customImpl.impl}`);
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
return parts.join("\n");
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
function getAllProperties(
|
|
1507
|
-
model: Model,
|
|
1508
|
-
_program: Program,
|
|
1509
|
-
): Map<string, ModelProperty> {
|
|
1510
|
-
const props = new Map<string, ModelProperty>();
|
|
1511
|
-
|
|
1512
|
-
if (model.baseModel) {
|
|
1513
|
-
const baseProps = getAllProperties(model.baseModel, _program);
|
|
1514
|
-
for (const [key, value] of baseProps) {
|
|
1515
|
-
props.set(key, value);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
for (const [key, value] of model.properties) {
|
|
1520
|
-
props.set(key, value);
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
return props;
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
function emitEnum(enumType: Enum): string {
|
|
1527
|
-
const parts: string[] = [];
|
|
1528
|
-
const name = toPascalCase(enumType.name);
|
|
1529
|
-
const members = Array.from(enumType.members.values());
|
|
1530
|
-
const isString = members.every(
|
|
1531
|
-
(m) => m.value === undefined || typeof m.value === "string",
|
|
1532
|
-
);
|
|
1533
|
-
|
|
1534
|
-
const baseDerives = [
|
|
1535
|
-
"Debug",
|
|
1536
|
-
"Clone",
|
|
1537
|
-
"Copy",
|
|
1538
|
-
"PartialEq",
|
|
1539
|
-
"Eq",
|
|
1540
|
-
"Hash",
|
|
1541
|
-
"Default",
|
|
1542
|
-
"serde::Serialize",
|
|
1543
|
-
"serde::Deserialize",
|
|
1544
|
-
];
|
|
1545
|
-
|
|
1546
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1547
|
-
const customDerives = (enumType as any)[rustDeriveKey] as
|
|
1548
|
-
| RustDeriveInfo
|
|
1549
|
-
| undefined;
|
|
1550
|
-
const allDerives = [...baseDerives];
|
|
1551
|
-
if (customDerives) {
|
|
1552
|
-
allDerives.push(...customDerives.derives);
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1556
|
-
const customAttrs = (enumType as any)[rustAttrKey] as
|
|
1557
|
-
| RustAttrInfo
|
|
1558
|
-
| undefined;
|
|
1559
|
-
const attrLines: string[] = [];
|
|
1560
|
-
if (customAttrs) {
|
|
1561
|
-
for (const attr of customAttrs.attrs) {
|
|
1562
|
-
attrLines.push(`#[${attr}]`);
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
if (isString) {
|
|
1567
|
-
parts.push(`#[derive(${allDerives.join(", ")})]`);
|
|
1568
|
-
if (attrLines.length > 0) {
|
|
1569
|
-
parts.push(...attrLines);
|
|
1570
|
-
}
|
|
1571
|
-
parts.push(`pub enum ${name} {`);
|
|
1572
|
-
for (let i = 0; i < members.length; i++) {
|
|
1573
|
-
const variantName = toRustVariantName(members[i].name);
|
|
1574
|
-
const serdeValue = members[i].value ?? members[i].name;
|
|
1575
|
-
if (i === 0) {
|
|
1576
|
-
parts.push(` #[default]`);
|
|
1577
|
-
}
|
|
1578
|
-
parts.push(` #[serde(rename = "${serdeValue}")]`);
|
|
1579
|
-
parts.push(` ${variantName},`);
|
|
1580
|
-
}
|
|
1581
|
-
} else {
|
|
1582
|
-
parts.push(`#[derive(${allDerives.join(", ")})]`);
|
|
1583
|
-
if (attrLines.length > 0) {
|
|
1584
|
-
parts.push(...attrLines);
|
|
1585
|
-
}
|
|
1586
|
-
parts.push(`pub enum ${name} {`);
|
|
1587
|
-
for (let i = 0; i < members.length; i++) {
|
|
1588
|
-
const variantName = toRustVariantName(members[i].name);
|
|
1589
|
-
const enumValue = members[i].value ?? 0;
|
|
1590
|
-
if (i === 0) {
|
|
1591
|
-
parts.push(` #[default]`);
|
|
1592
|
-
}
|
|
1593
|
-
parts.push(` ${variantName} = ${enumValue},`);
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
parts.push("}");
|
|
1597
|
-
return parts.join("\n");
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
function emitUnion(union: Union, program: Program): string {
|
|
1601
|
-
const parts: string[] = [];
|
|
1602
|
-
const name = toPascalCase(union.name ?? "Union");
|
|
1603
|
-
|
|
1604
|
-
const variants = Array.from(union.variants.values());
|
|
1605
|
-
if (variants.length === 0) {
|
|
1606
|
-
parts.push(
|
|
1607
|
-
`#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub enum ${name} {}\nimpl ${name} {\n pub fn new() -> Self { unreachable!() }\n}`,
|
|
1608
|
-
);
|
|
1609
|
-
return parts.join("\n");
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
parts.push(
|
|
1613
|
-
`#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\n#[serde(untagged)]\npub enum ${name} {`,
|
|
1614
|
-
);
|
|
1615
|
-
for (let i = 0; i < variants.length; i++) {
|
|
1616
|
-
const variant = variants[i];
|
|
1617
|
-
const variantName = `Variant${i + 1}`;
|
|
1618
|
-
const { type: rustType } = getRustTypeForProperty(
|
|
1619
|
-
variant.type,
|
|
1620
|
-
program,
|
|
1621
|
-
new Map(),
|
|
1622
|
-
);
|
|
1623
|
-
parts.push(` ${variantName}(${rustType}),`);
|
|
1624
|
-
}
|
|
1625
|
-
parts.push("}");
|
|
1626
|
-
|
|
1627
|
-
return parts.join("\n");
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
function emitScalar(
|
|
1631
|
-
scalar: Scalar,
|
|
1632
|
-
program: Program,
|
|
1633
|
-
): { typeDef: string; impls: string[] } {
|
|
1634
|
-
const format = getFormat(program, scalar);
|
|
1635
|
-
const pattern = getPattern(program, scalar);
|
|
1636
|
-
const structName = toPascalCase(scalar.name);
|
|
1637
|
-
const impls: string[] = [];
|
|
1638
|
-
|
|
1639
|
-
if (format && formatToRust[format]) {
|
|
1640
|
-
return {
|
|
1641
|
-
typeDef: `pub type ${structName} = ${formatToRust[format]};`,
|
|
1642
|
-
impls: [],
|
|
1643
|
-
};
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
if (pattern) {
|
|
1647
|
-
const rustType = scalarToRust[scalar.name] ?? "String";
|
|
1648
|
-
impls.push(
|
|
1649
|
-
`\nimpl TryFrom<String> for ${structName} {\n type Error = String;\n\n fn try_from(value: String) -> Result<Self, Self::Error> {\n let re = regex::Regex::new(r"${pattern}").unwrap();\n if re.is_match(&value) { Ok(Self(value)) } else { Err(format!("Invalid value: {}", value)) }\n }\n}`,
|
|
1650
|
-
);
|
|
1651
|
-
|
|
1652
|
-
return {
|
|
1653
|
-
typeDef: `#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]\npub struct ${structName}(pub ${rustType});`,
|
|
1654
|
-
impls,
|
|
1655
|
-
};
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
const rustType = scalarToRust[scalar.name] ?? "serde_json::Value";
|
|
1659
|
-
return {
|
|
1660
|
-
typeDef: `pub type ${structName} = ${rustType};`,
|
|
1661
|
-
impls: [],
|
|
1662
|
-
};
|
|
1663
|
-
}
|
|
11
|
+
import {
|
|
12
|
+
RustEmitterOptions,
|
|
13
|
+
AnonymousStringLiteralUnion,
|
|
14
|
+
} from "./models/types.js";
|
|
15
|
+
import { isStdLibType, getAllOperations } from "./parser/index.js";
|
|
16
|
+
import {
|
|
17
|
+
emitModel,
|
|
18
|
+
emitEnum,
|
|
19
|
+
emitUnion,
|
|
20
|
+
emitScalar,
|
|
21
|
+
emitStringLiteralUnion,
|
|
22
|
+
} from "./generator/types_file.js";
|
|
23
|
+
import { generateServerTrait } from "./generator/server_trait.js";
|
|
24
|
+
import { generateResponseEnums } from "./generator/response_enums.js";
|
|
25
|
+
import { generateRouter } from "./generator/router.js";
|
|
1664
26
|
|
|
1665
27
|
export async function $onEmit(
|
|
1666
28
|
context: EmitContext<RustEmitterOptions>,
|
|
@@ -1738,7 +100,9 @@ export async function $onEmit(
|
|
|
1738
100
|
);
|
|
1739
101
|
for (let i = 0; i < anonEnum.variants.length; i++) {
|
|
1740
102
|
const literal = anonEnum.variants[i];
|
|
1741
|
-
const variantName =
|
|
103
|
+
const variantName = (
|
|
104
|
+
literal.value.charAt(0).toUpperCase() + literal.value.slice(1)
|
|
105
|
+
).replace(/[^a-zA-Z0-9]/g, "");
|
|
1742
106
|
if (i === 0) {
|
|
1743
107
|
parts.push(` #[default]`);
|
|
1744
108
|
}
|