typespec-rust-emitter 0.1.0 → 0.3.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/.qwen/settings.json +11 -0
- package/CHANGELOG.md +86 -0
- package/DEV.md +81 -0
- package/QWEN.md +238 -0
- package/README.md +194 -4
- package/dist/src/emitter.d.ts +2 -0
- package/dist/src/emitter.js +662 -16
- package/dist/src/emitter.js.map +1 -1
- 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/test/hello.test.js +78 -0
- package/dist/test/hello.test.js.map +1 -1
- package/example/lib/learning/models.tsp +2 -0
- package/example/output-rust/Cargo.lock +639 -1
- package/example/output-rust/Cargo.toml +9 -1
- package/example/output-rust/src/generated/mod.rs +2 -1
- package/example/output-rust/src/generated/server.rs +712 -0
- package/example/output-rust/src/generated/types.rs +20 -42
- package/example/package-lock.json +1 -2
- package/justfile +4 -1
- package/package.json +8 -3
- package/src/emitter.ts +868 -15
- package/src/index.ts +1 -1
- package/src/lib.tsp +4 -2
- package/test/hello.test.ts +100 -0
package/dist/src/emitter.js
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
1
|
-
import { emitFile, resolvePath, navigateProgram, getDoc, isArrayModelType, isRecordModelType, getFormat, getPattern, isErrorModel, getNamespaceFullName, } from "@typespec/compiler";
|
|
1
|
+
import { emitFile, resolvePath, navigateProgram, getDoc, isArrayModelType, isRecordModelType, getFormat, getPattern, isErrorModel, getNamespaceFullName, getTags, } from "@typespec/compiler";
|
|
2
2
|
const rustDeriveKey = Symbol("rustDerive");
|
|
3
|
+
const rustAttrKey = Symbol("rustAttr");
|
|
3
4
|
export function $rustDerive(context, target, derive) {
|
|
4
|
-
if (target.kind !== "Model") {
|
|
5
|
+
if (target.kind !== "Model" && target.kind !== "Enum") {
|
|
5
6
|
context.program.reportDiagnostic({
|
|
6
7
|
code: "rust-derive-invalid-target",
|
|
7
|
-
message: `@rustDerive can only be applied to models`,
|
|
8
|
+
message: `@rustDerive can only be applied to models and enums`,
|
|
8
9
|
severity: "error",
|
|
9
10
|
target: context.decoratorTarget,
|
|
10
11
|
});
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
13
|
-
const
|
|
14
|
-
|
|
14
|
+
const ns = target.kind === "Model"
|
|
15
|
+
? target.namespace
|
|
16
|
+
? getNamespaceFullName(target.namespace)
|
|
17
|
+
: ""
|
|
18
|
+
: target.namespace
|
|
19
|
+
? getNamespaceFullName(target.namespace)
|
|
20
|
+
: "";
|
|
15
21
|
if (!ns.startsWith("TypeSpec")) {
|
|
16
|
-
const info =
|
|
22
|
+
const info = target[rustDeriveKey];
|
|
17
23
|
if (info) {
|
|
18
24
|
if (!info.derives.includes(derive)) {
|
|
19
25
|
info.derives.push(derive);
|
|
20
26
|
}
|
|
21
27
|
}
|
|
22
28
|
else {
|
|
23
|
-
|
|
29
|
+
target[rustDeriveKey] = { derives: [derive] };
|
|
24
30
|
}
|
|
25
31
|
}
|
|
26
32
|
}
|
|
@@ -29,6 +35,591 @@ export function $rustDerives(context, target, ...derives) {
|
|
|
29
35
|
$rustDerive(context, target, derive);
|
|
30
36
|
}
|
|
31
37
|
}
|
|
38
|
+
export function $rustAttr(context, target, attr) {
|
|
39
|
+
if (target.kind !== "Model" && target.kind !== "Enum") {
|
|
40
|
+
context.program.reportDiagnostic({
|
|
41
|
+
code: "rust-attr-invalid-target",
|
|
42
|
+
message: `@rustAttr can only be applied to models and enums`,
|
|
43
|
+
severity: "error",
|
|
44
|
+
target: context.decoratorTarget,
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const ns = target.kind === "Model"
|
|
49
|
+
? target.namespace
|
|
50
|
+
? getNamespaceFullName(target.namespace)
|
|
51
|
+
: ""
|
|
52
|
+
: target.namespace
|
|
53
|
+
? getNamespaceFullName(target.namespace)
|
|
54
|
+
: "";
|
|
55
|
+
if (!ns.startsWith("TypeSpec")) {
|
|
56
|
+
const info = target[rustAttrKey];
|
|
57
|
+
if (info) {
|
|
58
|
+
if (!info.attrs.includes(attr)) {
|
|
59
|
+
info.attrs.push(attr);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
target[rustAttrKey] = { attrs: [attr] };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function $rustAttrs(context, target, ...attrs) {
|
|
68
|
+
for (const attr of attrs) {
|
|
69
|
+
$rustAttr(context, target, attr);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function getDecoratorName(decorator) {
|
|
73
|
+
if (!decorator)
|
|
74
|
+
return "";
|
|
75
|
+
if (typeof decorator !== "object")
|
|
76
|
+
return "";
|
|
77
|
+
if (decorator.node?.target?.sv) {
|
|
78
|
+
return decorator.node.target.sv;
|
|
79
|
+
}
|
|
80
|
+
if (decorator.node?.target?.kind === "Identifier" &&
|
|
81
|
+
decorator.node?.target?.sv) {
|
|
82
|
+
return decorator.node.target.sv;
|
|
83
|
+
}
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
function getDecoratorArgValue(decorator, index = 0) {
|
|
87
|
+
if (!decorator)
|
|
88
|
+
return "";
|
|
89
|
+
const args = decorator.args || decorator.arguments;
|
|
90
|
+
if (!args)
|
|
91
|
+
return "";
|
|
92
|
+
const arg = args[index];
|
|
93
|
+
if (arg?.jsValue !== undefined)
|
|
94
|
+
return String(arg.jsValue);
|
|
95
|
+
if (arg?.value !== undefined)
|
|
96
|
+
return String(arg.value);
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
function getHttpMethod(_program, operation) {
|
|
100
|
+
const decorators = operation.decorators;
|
|
101
|
+
if (!decorators)
|
|
102
|
+
return undefined;
|
|
103
|
+
for (const key of Object.keys(decorators)) {
|
|
104
|
+
const decorator = decorators[key];
|
|
105
|
+
const name = getDecoratorName(decorator);
|
|
106
|
+
if (name === "get")
|
|
107
|
+
return "GET";
|
|
108
|
+
if (name === "post")
|
|
109
|
+
return "POST";
|
|
110
|
+
if (name === "put")
|
|
111
|
+
return "PUT";
|
|
112
|
+
if (name === "patch")
|
|
113
|
+
return "PATCH";
|
|
114
|
+
if (name === "delete")
|
|
115
|
+
return "DELETE";
|
|
116
|
+
if (name === "head")
|
|
117
|
+
return "HEAD";
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
function getRoute(_program, target) {
|
|
122
|
+
const decorators = target.decorators;
|
|
123
|
+
if (!decorators)
|
|
124
|
+
return "";
|
|
125
|
+
for (const key of Object.keys(decorators)) {
|
|
126
|
+
const decorator = decorators[key];
|
|
127
|
+
const name = getDecoratorName(decorator);
|
|
128
|
+
if (name === "route") {
|
|
129
|
+
return getDecoratorArgValue(decorator, 0);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
function hasAuthDecorator(operation) {
|
|
135
|
+
const decorators = operation.decorators;
|
|
136
|
+
if (!decorators)
|
|
137
|
+
return false;
|
|
138
|
+
for (const key of Object.keys(decorators)) {
|
|
139
|
+
const decorator = decorators[key];
|
|
140
|
+
const name = getDecoratorName(decorator);
|
|
141
|
+
if (name === "useAuth") {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
function getOperationParameters(program, operation, anonymousEnums) {
|
|
148
|
+
const params = [];
|
|
149
|
+
const model = operation.parameters;
|
|
150
|
+
for (const [propName, prop] of model.properties) {
|
|
151
|
+
const decorators = prop.decorators;
|
|
152
|
+
let location = "query";
|
|
153
|
+
let rustName = toRustIdent(propName);
|
|
154
|
+
if (decorators) {
|
|
155
|
+
if (decorators["$path"]) {
|
|
156
|
+
location = "path";
|
|
157
|
+
}
|
|
158
|
+
else if (decorators["$query"]) {
|
|
159
|
+
location = "query";
|
|
160
|
+
}
|
|
161
|
+
else if (decorators["$header"]) {
|
|
162
|
+
location = "header";
|
|
163
|
+
const headerDec = decorators["$header"];
|
|
164
|
+
if (headerDec?.value) {
|
|
165
|
+
rustName = headerDec.value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (decorators["$cookie"]) {
|
|
169
|
+
location = "cookie";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const { type: rustType } = getRustTypeForProperty(prop.type, program, anonymousEnums);
|
|
173
|
+
params.push({
|
|
174
|
+
name: propName,
|
|
175
|
+
rustName,
|
|
176
|
+
rustType,
|
|
177
|
+
location,
|
|
178
|
+
required: !prop.optional,
|
|
179
|
+
optional: prop.optional,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return params;
|
|
183
|
+
}
|
|
184
|
+
function getOperationBody(operation) {
|
|
185
|
+
for (const [_propName, prop] of operation.parameters.properties) {
|
|
186
|
+
const decorators = prop.decorators;
|
|
187
|
+
if (decorators?.["$body"] || decorators?.["$bodyRoot"]) {
|
|
188
|
+
return prop;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
function getOperationResponses(program, operation, anonymousEnums) {
|
|
194
|
+
const responses = [];
|
|
195
|
+
const returnType = operation.returnType;
|
|
196
|
+
if (returnType.kind === "Union") {
|
|
197
|
+
const union = returnType;
|
|
198
|
+
for (const variant of union.variants.values()) {
|
|
199
|
+
const statusCode = getStatusCode(variant);
|
|
200
|
+
const bodyInfo = getBodyFromResponse(variant, program, anonymousEnums);
|
|
201
|
+
responses.push({
|
|
202
|
+
statusCode,
|
|
203
|
+
bodyType: bodyInfo.type,
|
|
204
|
+
bodyDescription: bodyInfo.description,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (returnType.kind === "Model") {
|
|
209
|
+
const model = returnType;
|
|
210
|
+
for (const [propName, prop] of model.properties) {
|
|
211
|
+
if (propName === "body") {
|
|
212
|
+
const { type: rustType } = getRustTypeForProperty(prop.type, program, anonymousEnums);
|
|
213
|
+
responses.push({
|
|
214
|
+
statusCode: 200,
|
|
215
|
+
bodyType: rustType,
|
|
216
|
+
bodyDescription: getDoc(program, prop),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return responses;
|
|
222
|
+
}
|
|
223
|
+
function getStatusCode(variant) {
|
|
224
|
+
if (variant.type.kind === "Model") {
|
|
225
|
+
const model = variant.type;
|
|
226
|
+
for (const [propName, prop] of model.properties) {
|
|
227
|
+
if (propName === "statusCode") {
|
|
228
|
+
const typeAny = prop.type;
|
|
229
|
+
if (typeAny.value !== undefined) {
|
|
230
|
+
return typeAny.value;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return 200;
|
|
236
|
+
}
|
|
237
|
+
function getBodyFromResponse(variant, program, anonymousEnums) {
|
|
238
|
+
if (variant.type.kind === "Model") {
|
|
239
|
+
const model = variant.type;
|
|
240
|
+
for (const [propName, prop] of model.properties) {
|
|
241
|
+
if (propName === "body") {
|
|
242
|
+
const { type: rustType } = getRustTypeForProperty(prop.type, program, anonymousEnums);
|
|
243
|
+
return { type: rustType, description: getDoc(program, prop) };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { type: undefined, description: undefined };
|
|
248
|
+
}
|
|
249
|
+
function getAllOperations(program) {
|
|
250
|
+
const seenNamespaces = new Set();
|
|
251
|
+
const namespaceOps = new Map();
|
|
252
|
+
navigateProgram(program, {
|
|
253
|
+
namespace(ns) {
|
|
254
|
+
const route = getRoute(program, ns);
|
|
255
|
+
if (!route)
|
|
256
|
+
return;
|
|
257
|
+
if (!seenNamespaces.has(ns.name)) {
|
|
258
|
+
seenNamespaces.add(ns.name);
|
|
259
|
+
namespaceOps.set(ns.name, { ns, ops: [] });
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
operation(op) {
|
|
263
|
+
const method = getHttpMethod(program, op);
|
|
264
|
+
if (!method)
|
|
265
|
+
return;
|
|
266
|
+
const ns = op.namespace;
|
|
267
|
+
if (!ns)
|
|
268
|
+
return;
|
|
269
|
+
if (!seenNamespaces.has(ns.name)) {
|
|
270
|
+
seenNamespaces.add(ns.name);
|
|
271
|
+
namespaceOps.set(ns.name, { ns, ops: [] });
|
|
272
|
+
}
|
|
273
|
+
const entry = namespaceOps.get(ns.name);
|
|
274
|
+
if (entry) {
|
|
275
|
+
entry.ops.push(op);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
const result = [];
|
|
280
|
+
for (const [, entry] of namespaceOps) {
|
|
281
|
+
if (entry.ops.length > 0) {
|
|
282
|
+
result.push({ namespace: entry.ns, operations: entry.ops });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
function buildFullPath(namespaceRoute, operationRoute) {
|
|
288
|
+
let fullPath = namespaceRoute + operationRoute;
|
|
289
|
+
fullPath = fullPath.replace(/\/+/g, "/");
|
|
290
|
+
if (fullPath.length > 1 && fullPath.endsWith("/")) {
|
|
291
|
+
fullPath = fullPath.slice(0, -1);
|
|
292
|
+
}
|
|
293
|
+
return fullPath;
|
|
294
|
+
}
|
|
295
|
+
function emitOperationInfo(program, op, nsRoute, anonymousEnums) {
|
|
296
|
+
const method = getHttpMethod(program, op);
|
|
297
|
+
if (!method)
|
|
298
|
+
return undefined;
|
|
299
|
+
const opRoute = getRoute(program, op);
|
|
300
|
+
const fullPath = buildFullPath(nsRoute, opRoute);
|
|
301
|
+
const tags = getTags(program, op) ?? [];
|
|
302
|
+
const params = getOperationParameters(program, op, anonymousEnums);
|
|
303
|
+
const body = getOperationBody(op);
|
|
304
|
+
const responses = getOperationResponses(program, op, anonymousEnums);
|
|
305
|
+
const doc = getDoc(program, op);
|
|
306
|
+
const opName = op.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
307
|
+
return {
|
|
308
|
+
name: opName,
|
|
309
|
+
method,
|
|
310
|
+
path: fullPath,
|
|
311
|
+
tags,
|
|
312
|
+
parameters: params,
|
|
313
|
+
body: body,
|
|
314
|
+
responses,
|
|
315
|
+
doc,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function getStatusVariantName(statusCode) {
|
|
319
|
+
const statusNames = {
|
|
320
|
+
200: "Ok",
|
|
321
|
+
201: "Created",
|
|
322
|
+
202: "Accepted",
|
|
323
|
+
204: "NoContent",
|
|
324
|
+
400: "BadRequest",
|
|
325
|
+
401: "Unauthorized",
|
|
326
|
+
403: "Forbidden",
|
|
327
|
+
404: "NotFound",
|
|
328
|
+
409: "Conflict",
|
|
329
|
+
500: "InternalServerError",
|
|
330
|
+
};
|
|
331
|
+
return statusNames[statusCode] || `Status${statusCode}`;
|
|
332
|
+
}
|
|
333
|
+
function getHttpStatusCode(statusCode) {
|
|
334
|
+
const statusCodes = {
|
|
335
|
+
200: "StatusCode::OK",
|
|
336
|
+
201: "StatusCode::CREATED",
|
|
337
|
+
202: "StatusCode::ACCEPTED",
|
|
338
|
+
204: "StatusCode::NO_CONTENT",
|
|
339
|
+
400: "StatusCode::BAD_REQUEST",
|
|
340
|
+
401: "StatusCode::UNAUTHORIZED",
|
|
341
|
+
403: "StatusCode::FORBIDDEN",
|
|
342
|
+
404: "StatusCode::NOT_FOUND",
|
|
343
|
+
409: "StatusCode::CONFLICT",
|
|
344
|
+
500: "StatusCode::INTERNAL_SERVER_ERROR",
|
|
345
|
+
};
|
|
346
|
+
return statusCodes[statusCode] || `StatusCode::from_u16(${statusCode})`;
|
|
347
|
+
}
|
|
348
|
+
function generateServerTrait(program, namespaceGroups, anonymousEnums) {
|
|
349
|
+
const parts = [];
|
|
350
|
+
parts.push(`use super::types::*;
|
|
351
|
+
use async_trait::async_trait;
|
|
352
|
+
use axum::{http::StatusCode, Json};
|
|
353
|
+
use eyre::Result;
|
|
354
|
+
|
|
355
|
+
#[async_trait]
|
|
356
|
+
pub trait Server: Send + Sync {
|
|
357
|
+
type Claims: Send + Sync + 'static;
|
|
358
|
+
|
|
359
|
+
`);
|
|
360
|
+
for (const group of namespaceGroups) {
|
|
361
|
+
const nsName = toPascalCase(group.namespace.name.replace(/[^a-zA-Z0-9_]/g, "_"));
|
|
362
|
+
for (const op of group.operations) {
|
|
363
|
+
const opInfo = emitOperationInfo(program, op, "", anonymousEnums);
|
|
364
|
+
if (!opInfo)
|
|
365
|
+
continue;
|
|
366
|
+
if (opInfo.doc) {
|
|
367
|
+
parts.push(` ${formatDoc(opInfo.doc)}`);
|
|
368
|
+
}
|
|
369
|
+
const requestName = `${nsName}${toPascalCase(opInfo.name)}Request`;
|
|
370
|
+
const responseName = `${nsName}${toPascalCase(opInfo.name)}Response`;
|
|
371
|
+
const fnName = toRustIdent(`${nsName}_${opInfo.name}`);
|
|
372
|
+
const isProtected = hasAuthDecorator(op);
|
|
373
|
+
if (isProtected) {
|
|
374
|
+
parts.push(` async fn ${fnName}(&self, claims: Self::Claims, request: ${requestName}) -> Result<${responseName}>;`);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
parts.push(` async fn ${fnName}(&self, request: ${requestName}) -> Result<${responseName}>;`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
parts.push("}");
|
|
382
|
+
return parts.join("\n");
|
|
383
|
+
}
|
|
384
|
+
function generateRequestStructs(program, namespaceGroups, anonymousEnums) {
|
|
385
|
+
const parts = [];
|
|
386
|
+
for (const group of namespaceGroups) {
|
|
387
|
+
const nsName = toPascalCase(group.namespace.name.replace(/[^a-zA-Z0-9_]/g, "_"));
|
|
388
|
+
for (const op of group.operations) {
|
|
389
|
+
const opInfo = emitOperationInfo(program, op, "", anonymousEnums);
|
|
390
|
+
if (!opInfo)
|
|
391
|
+
continue;
|
|
392
|
+
const params = opInfo.parameters;
|
|
393
|
+
const requestName = `${nsName}${toPascalCase(opInfo.name)}Request`;
|
|
394
|
+
const fields = [];
|
|
395
|
+
for (const param of params) {
|
|
396
|
+
const rustType = param.optional
|
|
397
|
+
? `Option<${param.rustType}>`
|
|
398
|
+
: param.rustType;
|
|
399
|
+
fields.push(` #[serde(rename = "${param.name}", flatten)]`);
|
|
400
|
+
fields.push(` pub ${param.rustName}: ${rustType},`);
|
|
401
|
+
}
|
|
402
|
+
if (opInfo.body) {
|
|
403
|
+
const bodyType = getRustTypeForProperty(opInfo.body.type, program, anonymousEnums);
|
|
404
|
+
fields.push(` #[serde(rename = "body")]`);
|
|
405
|
+
fields.push(` pub body: ${bodyType.type},`);
|
|
406
|
+
}
|
|
407
|
+
parts.push(`#[derive(Debug, Clone, serde::Deserialize)]
|
|
408
|
+
pub struct ${requestName} {
|
|
409
|
+
${fields.join("\n")}
|
|
410
|
+
}
|
|
411
|
+
`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return parts.join("\n");
|
|
415
|
+
}
|
|
416
|
+
function generateResponseEnums(program, namespaceGroups, anonymousEnums) {
|
|
417
|
+
const parts = [];
|
|
418
|
+
for (const group of namespaceGroups) {
|
|
419
|
+
const nsName = toPascalCase(group.namespace.name.replace(/[^a-zA-Z0-9_]/g, "_"));
|
|
420
|
+
for (const op of group.operations) {
|
|
421
|
+
const opInfo = emitOperationInfo(program, op, "", anonymousEnums);
|
|
422
|
+
if (!opInfo)
|
|
423
|
+
continue;
|
|
424
|
+
if (opInfo.responses.length === 0)
|
|
425
|
+
continue;
|
|
426
|
+
const responseName = `${nsName}${toPascalCase(opInfo.name)}Response`;
|
|
427
|
+
const variants = [];
|
|
428
|
+
for (const resp of opInfo.responses) {
|
|
429
|
+
const variantName = getStatusVariantName(resp.statusCode);
|
|
430
|
+
if (!resp.bodyType) {
|
|
431
|
+
variants.push(` ${variantName},`);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
variants.push(` ${variantName}(Json<${resp.bodyType}>),`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
parts.push(`pub enum ${responseName} {
|
|
438
|
+
${variants.join("\n")}
|
|
439
|
+
}
|
|
440
|
+
`);
|
|
441
|
+
parts.push(`impl IntoResponse for ${responseName} {
|
|
442
|
+
fn into_response(self) -> axum::response::Response {
|
|
443
|
+
match self {
|
|
444
|
+
`);
|
|
445
|
+
for (const resp of opInfo.responses) {
|
|
446
|
+
const variantName = getStatusVariantName(resp.statusCode);
|
|
447
|
+
const statusCodeStr = getHttpStatusCode(resp.statusCode);
|
|
448
|
+
if (!resp.bodyType) {
|
|
449
|
+
parts.push(` ${responseName}::${variantName} => ${statusCodeStr}.into_response(),`);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
parts.push(` ${responseName}::${variantName}(body) => (${statusCodeStr}, body).into_response(),`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
parts.push(` }
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return parts.join("\n");
|
|
462
|
+
}
|
|
463
|
+
function generateRouter(program, namespaceGroups, anonymousEnums) {
|
|
464
|
+
const handlers = [];
|
|
465
|
+
const publicRoutes = [];
|
|
466
|
+
const protectedRoutes = [];
|
|
467
|
+
const usedMethods = new Set();
|
|
468
|
+
for (const group of namespaceGroups) {
|
|
469
|
+
const nsRoute = getRoute(program, group.namespace);
|
|
470
|
+
if (!nsRoute)
|
|
471
|
+
continue;
|
|
472
|
+
const nsName = toPascalCase(group.namespace.name.replace(/[^a-zA-Z0-9_]/g, "_"));
|
|
473
|
+
for (const op of group.operations) {
|
|
474
|
+
const opInfo = emitOperationInfo(program, op, nsRoute, anonymousEnums);
|
|
475
|
+
if (!opInfo)
|
|
476
|
+
continue;
|
|
477
|
+
const method = opInfo.method.toLowerCase();
|
|
478
|
+
usedMethods.add(method);
|
|
479
|
+
const handlerFnName = toRustIdent(`${nsName}_${opInfo.name}`);
|
|
480
|
+
const traitFnName = handlerFnName;
|
|
481
|
+
const requestName = `${nsName}${toPascalCase(opInfo.name)}Request`;
|
|
482
|
+
const isProtected = hasAuthDecorator(op);
|
|
483
|
+
const pathParams = opInfo.parameters.filter((p) => p.location === "path");
|
|
484
|
+
const queryParams = opInfo.parameters.filter((p) => p.location === "query");
|
|
485
|
+
const hasPathParams = pathParams.length > 0;
|
|
486
|
+
const hasQueryParams = queryParams.length > 0;
|
|
487
|
+
const hasBody = !!opInfo.body;
|
|
488
|
+
// Build extractor lines and request construction expression
|
|
489
|
+
const extractorLines = [];
|
|
490
|
+
let requestExpr = "";
|
|
491
|
+
if (hasPathParams) {
|
|
492
|
+
const pathTypes = pathParams.map((p) => p.rustType).join(", ");
|
|
493
|
+
const pathFields = pathParams.map((p) => p.rustName).join(", ");
|
|
494
|
+
if (pathParams.length === 1) {
|
|
495
|
+
extractorLines.push(` axum::extract::Path(${pathFields}): axum::extract::Path<${pathTypes}>,`);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
extractorLines.push(` axum::extract::Path((${pathFields})): axum::extract::Path<(${pathTypes})>,`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (hasQueryParams) {
|
|
502
|
+
extractorLines.push(` axum::extract::Query(query): axum::extract::Query<${requestName}>,`);
|
|
503
|
+
}
|
|
504
|
+
else if (hasBody) {
|
|
505
|
+
extractorLines.push(` axum::Json(body): axum::Json<${requestName}Body>,`);
|
|
506
|
+
}
|
|
507
|
+
if (isProtected) {
|
|
508
|
+
extractorLines.push(` axum::Extension(claims): axum::Extension<S::Claims>,`);
|
|
509
|
+
}
|
|
510
|
+
// Build request struct expression
|
|
511
|
+
if (hasOnlyPathParams(hasPathParams, hasQueryParams, hasBody)) {
|
|
512
|
+
const pathAssignments = pathParams
|
|
513
|
+
.map((p) => `${p.rustName},`)
|
|
514
|
+
.join(" ");
|
|
515
|
+
requestExpr = `${requestName} { ${pathAssignments} }`;
|
|
516
|
+
}
|
|
517
|
+
else if (hasQueryParams) {
|
|
518
|
+
if (hasPathParams) {
|
|
519
|
+
const pathAssignments = pathParams
|
|
520
|
+
.map((p) => `${p.rustName},`)
|
|
521
|
+
.join(" ");
|
|
522
|
+
requestExpr = `${requestName} { ${pathAssignments} ..query }`;
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
requestExpr = "query";
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
else if (hasBody) {
|
|
529
|
+
if (hasPathParams) {
|
|
530
|
+
const pathAssignments = pathParams
|
|
531
|
+
.map((p) => `${p.rustName},`)
|
|
532
|
+
.join(" ");
|
|
533
|
+
requestExpr = `${requestName} { ${pathAssignments} body }`;
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
requestExpr = `${requestName} { body }`;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
requestExpr = `${requestName} {}`;
|
|
541
|
+
}
|
|
542
|
+
// Server method call
|
|
543
|
+
const serverCall = isProtected
|
|
544
|
+
? `service.${traitFnName}(claims, ${requestExpr}).await`
|
|
545
|
+
: `service.${traitFnName}(${requestExpr}).await`;
|
|
546
|
+
// All handlers use <S> generics, Claims is now an associated type
|
|
547
|
+
let handlerCode = `pub async fn ${handlerFnName}_handler<S>(
|
|
548
|
+
axum::extract::State(service): axum::extract::State<S>,
|
|
549
|
+
${extractorLines.join("\n")}
|
|
550
|
+
) -> impl axum::response::IntoResponse
|
|
551
|
+
where
|
|
552
|
+
S: Server + Clone + Send + Sync + 'static,
|
|
553
|
+
S::Claims: Send + Sync + Clone + 'static,
|
|
554
|
+
{
|
|
555
|
+
let result = ${serverCall};
|
|
556
|
+
match result {
|
|
557
|
+
Ok(response) => response.into_response(),
|
|
558
|
+
Err(e) => (
|
|
559
|
+
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
|
560
|
+
format!("Internal error: {e}"),
|
|
561
|
+
)
|
|
562
|
+
.into_response(),
|
|
563
|
+
}
|
|
564
|
+
}`;
|
|
565
|
+
handlers.push(handlerCode);
|
|
566
|
+
let routePath = `"${opInfo.path}"`;
|
|
567
|
+
let routeStmt = "";
|
|
568
|
+
if (isProtected) {
|
|
569
|
+
routeStmt = `.route(${routePath}, ${method}(${handlerFnName}_handler::<S>))`;
|
|
570
|
+
protectedRoutes.push(routeStmt);
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
routeStmt = `.route(${routePath}, ${method}(${handlerFnName}_handler::<S>))`;
|
|
574
|
+
publicRoutes.push(routeStmt);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const methodImports = Array.from(usedMethods).sort().join(", ");
|
|
579
|
+
const routerBody = buildRouterBody(publicRoutes, protectedRoutes);
|
|
580
|
+
const parts = [];
|
|
581
|
+
parts.push(`use axum::response::IntoResponse;
|
|
582
|
+
use axum::routing::{${methodImports}};
|
|
583
|
+
use axum::Router;
|
|
584
|
+
|
|
585
|
+
`);
|
|
586
|
+
parts.push(handlers.join("\n\n"));
|
|
587
|
+
parts.push(`
|
|
588
|
+
pub fn create_router<S, M>(service: S, middleware: M) -> Router
|
|
589
|
+
where
|
|
590
|
+
S: Server + Clone + Send + Sync + 'static,
|
|
591
|
+
S::Claims: Send + Sync + Clone + 'static,
|
|
592
|
+
M: FnOnce(Router<S>) -> Router<S> + Clone + Send + Sync + 'static,
|
|
593
|
+
{
|
|
594
|
+
${routerBody}
|
|
595
|
+
}`);
|
|
596
|
+
return parts.join("\n");
|
|
597
|
+
}
|
|
598
|
+
function hasOnlyPathParams(hasPath, hasQuery, hasBody) {
|
|
599
|
+
return hasPath && !hasQuery && !hasBody;
|
|
600
|
+
}
|
|
601
|
+
function buildRouterBody(publicRoutes, protectedRoutes) {
|
|
602
|
+
const lines = [];
|
|
603
|
+
lines.push(" let mut router = Router::new();");
|
|
604
|
+
if (publicRoutes.length > 0) {
|
|
605
|
+
lines.push(" let public = Router::new()");
|
|
606
|
+
for (const r of publicRoutes) {
|
|
607
|
+
lines.push(` ${r.trim()}`);
|
|
608
|
+
}
|
|
609
|
+
lines.push(" ;");
|
|
610
|
+
lines.push(" router = router.merge(public);");
|
|
611
|
+
}
|
|
612
|
+
if (protectedRoutes.length > 0) {
|
|
613
|
+
lines.push(" let protected = Router::new()");
|
|
614
|
+
for (const r of protectedRoutes) {
|
|
615
|
+
lines.push(` ${r.trim()}`);
|
|
616
|
+
}
|
|
617
|
+
lines.push(" ;");
|
|
618
|
+
lines.push(" router = router.merge(middleware(protected));");
|
|
619
|
+
}
|
|
620
|
+
lines.push(" router.with_state(service)");
|
|
621
|
+
return lines.join("\n");
|
|
622
|
+
}
|
|
32
623
|
const scalarToRust = {
|
|
33
624
|
string: "String",
|
|
34
625
|
int8: "i8",
|
|
@@ -284,9 +875,14 @@ function emitModel(model, program, anonymousEnums) {
|
|
|
284
875
|
if (isError && allProps.size > 0) {
|
|
285
876
|
parts.push('#[error("{code}: {message}")]');
|
|
286
877
|
}
|
|
287
|
-
|
|
878
|
+
const customAttrs = model[rustAttrKey];
|
|
879
|
+
if (customAttrs) {
|
|
880
|
+
for (const attr of customAttrs.attrs) {
|
|
881
|
+
parts.push(`#[${attr}]`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
288
884
|
if (allProps.size > 0) {
|
|
289
|
-
|
|
885
|
+
const fields = [];
|
|
290
886
|
for (const [propName, prop] of allProps) {
|
|
291
887
|
const doc = getDoc(program, prop);
|
|
292
888
|
const { type: rustType } = getRustTypeForProperty(prop.type, program, anonymousEnums);
|
|
@@ -294,19 +890,22 @@ function emitModel(model, program, anonymousEnums) {
|
|
|
294
890
|
const fieldName = toRustIdent(propName);
|
|
295
891
|
const serdeRename = propName !== fieldName ? `#[serde(rename = "${propName}")]` : "";
|
|
296
892
|
if (doc) {
|
|
297
|
-
|
|
893
|
+
fields.push(` ${formatDoc(doc)}`);
|
|
298
894
|
}
|
|
299
895
|
if (serdeRename) {
|
|
300
|
-
|
|
896
|
+
fields.push(` ${serdeRename}`);
|
|
301
897
|
}
|
|
302
898
|
if (optional) {
|
|
303
|
-
|
|
899
|
+
fields.push(` #[serde(skip_serializing_if = "Option::is_none")]`);
|
|
304
900
|
}
|
|
305
|
-
|
|
901
|
+
fields.push(` pub ${fieldName}: ${optional ? `Option<${rustType}>` : rustType},`);
|
|
306
902
|
}
|
|
307
|
-
parts.push(
|
|
903
|
+
parts.push(`pub struct ${name} {
|
|
904
|
+
${fields.join("\n")}
|
|
905
|
+
}`);
|
|
308
906
|
}
|
|
309
907
|
else {
|
|
908
|
+
parts.push(`pub struct ${name}`);
|
|
310
909
|
parts.push("(());");
|
|
311
910
|
}
|
|
312
911
|
return parts.join("\n");
|
|
@@ -329,8 +928,34 @@ function emitEnum(enumType) {
|
|
|
329
928
|
const name = toPascalCase(enumType.name);
|
|
330
929
|
const members = Array.from(enumType.members.values());
|
|
331
930
|
const isString = members.every((m) => m.value === undefined || typeof m.value === "string");
|
|
931
|
+
const baseDerives = [
|
|
932
|
+
"Debug",
|
|
933
|
+
"Clone",
|
|
934
|
+
"Copy",
|
|
935
|
+
"PartialEq",
|
|
936
|
+
"Eq",
|
|
937
|
+
"Hash",
|
|
938
|
+
"serde::Serialize",
|
|
939
|
+
"serde::Deserialize",
|
|
940
|
+
];
|
|
941
|
+
const customDerives = enumType[rustDeriveKey];
|
|
942
|
+
const allDerives = [...baseDerives];
|
|
943
|
+
if (customDerives) {
|
|
944
|
+
allDerives.push(...customDerives.derives);
|
|
945
|
+
}
|
|
946
|
+
const customAttrs = enumType[rustAttrKey];
|
|
947
|
+
const attrLines = [];
|
|
948
|
+
if (customAttrs) {
|
|
949
|
+
for (const attr of customAttrs.attrs) {
|
|
950
|
+
attrLines.push(`#[${attr}]`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
332
953
|
if (isString) {
|
|
333
|
-
parts.push(`#[derive(
|
|
954
|
+
parts.push(`#[derive(${allDerives.join(", ")})]`);
|
|
955
|
+
if (attrLines.length > 0) {
|
|
956
|
+
parts.push(...attrLines);
|
|
957
|
+
}
|
|
958
|
+
parts.push(`pub enum ${name} {`);
|
|
334
959
|
for (const member of members) {
|
|
335
960
|
const variantName = toRustVariantName(member.name);
|
|
336
961
|
const serdeValue = member.value ?? member.name;
|
|
@@ -339,7 +964,11 @@ function emitEnum(enumType) {
|
|
|
339
964
|
}
|
|
340
965
|
}
|
|
341
966
|
else {
|
|
342
|
-
parts.push(`#[derive(
|
|
967
|
+
parts.push(`#[derive(${allDerives.join(", ")})]`);
|
|
968
|
+
if (attrLines.length > 0) {
|
|
969
|
+
parts.push(...attrLines);
|
|
970
|
+
}
|
|
971
|
+
parts.push(`pub enum ${name} {`);
|
|
343
972
|
for (const member of members) {
|
|
344
973
|
const variantName = toRustVariantName(member.name);
|
|
345
974
|
const enumValue = member.value ?? 0;
|
|
@@ -481,10 +1110,27 @@ export async function $onEmit(context, _options) {
|
|
|
481
1110
|
}
|
|
482
1111
|
content.push("");
|
|
483
1112
|
}
|
|
1113
|
+
const namespaceGroups = getAllOperations(context.program);
|
|
484
1114
|
const outputContent = content.join("\n");
|
|
485
1115
|
await emitFile(context.program, {
|
|
486
1116
|
path: resolvePath(context.emitterOutputDir, "types.rs"),
|
|
487
1117
|
content: outputContent,
|
|
488
1118
|
});
|
|
1119
|
+
if (namespaceGroups.length > 0) {
|
|
1120
|
+
const serverTrait = generateServerTrait(context.program, namespaceGroups, anonymousEnums);
|
|
1121
|
+
const requestStructs = generateRequestStructs(context.program, namespaceGroups, anonymousEnums);
|
|
1122
|
+
const responseEnums = generateResponseEnums(context.program, namespaceGroups, anonymousEnums);
|
|
1123
|
+
const router = generateRouter(context.program, namespaceGroups, anonymousEnums);
|
|
1124
|
+
const serverContent = [
|
|
1125
|
+
serverTrait,
|
|
1126
|
+
requestStructs,
|
|
1127
|
+
responseEnums,
|
|
1128
|
+
router,
|
|
1129
|
+
].join("\n");
|
|
1130
|
+
await emitFile(context.program, {
|
|
1131
|
+
path: resolvePath(context.emitterOutputDir, "server.rs"),
|
|
1132
|
+
content: serverContent,
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
489
1135
|
}
|
|
490
1136
|
//# sourceMappingURL=emitter.js.map
|