typespec-rust-emitter 0.5.0 → 0.7.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/CHANGELOG.md +80 -0
- package/dist/src/emitter.js +56 -16
- package/dist/src/emitter.js.map +1 -1
- package/example/lib/learning/operations.tsp +27 -13
- package/example/output-rust/src/generated/server.rs +127 -124
- package/example/output-rust/src/generated/types.rs +3 -0
- package/example/package-lock.json +1 -1
- package/package.json +1 -1
- package/src/emitter.ts +70 -23
package/package.json
CHANGED
package/src/emitter.ts
CHANGED
|
@@ -65,12 +65,14 @@ export function $rustDerive(
|
|
|
65
65
|
: "";
|
|
66
66
|
|
|
67
67
|
if (!ns.startsWith("TypeSpec")) {
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
69
|
const info = (target as any)[rustDeriveKey] as RustDeriveInfo | undefined;
|
|
69
70
|
if (info) {
|
|
70
71
|
if (!info.derives.includes(derive)) {
|
|
71
72
|
info.derives.push(derive);
|
|
72
73
|
}
|
|
73
74
|
} else {
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
76
|
(target as any)[rustDeriveKey] = { derives: [derive] };
|
|
75
77
|
}
|
|
76
78
|
}
|
|
@@ -111,12 +113,14 @@ export function $rustAttr(
|
|
|
111
113
|
: "";
|
|
112
114
|
|
|
113
115
|
if (!ns.startsWith("TypeSpec")) {
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
117
|
const info = (target as any)[rustAttrKey] as RustAttrInfo | undefined;
|
|
115
118
|
if (info) {
|
|
116
119
|
if (!info.attrs.includes(attr)) {
|
|
117
120
|
info.attrs.push(attr);
|
|
118
121
|
}
|
|
119
122
|
} else {
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
120
124
|
(target as any)[rustAttrKey] = { attrs: [attr] };
|
|
121
125
|
}
|
|
122
126
|
}
|
|
@@ -160,7 +164,9 @@ interface ResponseInfo {
|
|
|
160
164
|
bodyDescription: string | undefined;
|
|
161
165
|
}
|
|
162
166
|
|
|
163
|
-
function getDecoratorName(decorator:
|
|
167
|
+
function getDecoratorName(decorator: {
|
|
168
|
+
node?: { target?: { sv?: string; kind?: string } };
|
|
169
|
+
}): string {
|
|
164
170
|
if (!decorator) return "";
|
|
165
171
|
if (typeof decorator !== "object") return "";
|
|
166
172
|
|
|
@@ -177,7 +183,13 @@ function getDecoratorName(decorator: any): string {
|
|
|
177
183
|
return "";
|
|
178
184
|
}
|
|
179
185
|
|
|
180
|
-
function getDecoratorArgValue(
|
|
186
|
+
function getDecoratorArgValue(
|
|
187
|
+
decorator: {
|
|
188
|
+
args?: { jsValue?: unknown; value?: unknown }[];
|
|
189
|
+
arguments?: { jsValue?: unknown; value?: unknown }[];
|
|
190
|
+
},
|
|
191
|
+
index: number = 0,
|
|
192
|
+
): string {
|
|
181
193
|
if (!decorator) return "";
|
|
182
194
|
const args = decorator.args || decorator.arguments;
|
|
183
195
|
if (!args) return "";
|
|
@@ -191,6 +203,7 @@ function getHttpMethod(
|
|
|
191
203
|
_program: Program,
|
|
192
204
|
operation: Operation,
|
|
193
205
|
): HttpMethod | undefined {
|
|
206
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
194
207
|
const decorators = (operation as any).decorators;
|
|
195
208
|
if (!decorators) return undefined;
|
|
196
209
|
|
|
@@ -208,6 +221,7 @@ function getHttpMethod(
|
|
|
208
221
|
}
|
|
209
222
|
|
|
210
223
|
function getRoute(_program: Program, target: Namespace | Operation): string {
|
|
224
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
211
225
|
const decorators = (target as any).decorators;
|
|
212
226
|
if (!decorators) return "";
|
|
213
227
|
|
|
@@ -221,7 +235,23 @@ function getRoute(_program: Program, target: Namespace | Operation): string {
|
|
|
221
235
|
return "";
|
|
222
236
|
}
|
|
223
237
|
|
|
238
|
+
function getFullRoute(program: Program, ns: Namespace): string {
|
|
239
|
+
const routes: string[] = [];
|
|
240
|
+
let current: Namespace | undefined = ns;
|
|
241
|
+
|
|
242
|
+
while (current) {
|
|
243
|
+
const route = getRoute(program, current);
|
|
244
|
+
if (route) {
|
|
245
|
+
routes.unshift(route);
|
|
246
|
+
}
|
|
247
|
+
current = current.namespace;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return routes.join("");
|
|
251
|
+
}
|
|
252
|
+
|
|
224
253
|
function hasAuthDecorator(operation: Operation): boolean {
|
|
254
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
225
255
|
const decorators = (operation as any).decorators;
|
|
226
256
|
if (!decorators) return false;
|
|
227
257
|
|
|
@@ -244,6 +274,7 @@ function getOperationParameters(
|
|
|
244
274
|
const model = operation.parameters;
|
|
245
275
|
|
|
246
276
|
for (const [propName, prop] of model.properties) {
|
|
277
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
247
278
|
const decorators = (prop as any).decorators;
|
|
248
279
|
|
|
249
280
|
// Skip body parameters - they are handled separately
|
|
@@ -308,6 +339,7 @@ function getOperationParameters(
|
|
|
308
339
|
|
|
309
340
|
function getOperationBody(operation: Operation): ModelProperty | undefined {
|
|
310
341
|
for (const [_propName, prop] of operation.parameters.properties) {
|
|
342
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
311
343
|
const decorators = (prop as any).decorators;
|
|
312
344
|
if (!decorators) continue;
|
|
313
345
|
|
|
@@ -367,6 +399,7 @@ function getStatusCode(variant: { type: Type }): number {
|
|
|
367
399
|
const model = variant.type as Model;
|
|
368
400
|
for (const [propName, prop] of model.properties) {
|
|
369
401
|
if (propName === "statusCode") {
|
|
402
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
370
403
|
const typeAny = prop.type as any;
|
|
371
404
|
if (typeAny.value !== undefined) {
|
|
372
405
|
return typeAny.value as number;
|
|
@@ -385,7 +418,20 @@ function getBodyFromResponse(
|
|
|
385
418
|
if (variant.type.kind === "Model") {
|
|
386
419
|
const model = variant.type as Model;
|
|
387
420
|
for (const [propName, prop] of model.properties) {
|
|
388
|
-
|
|
421
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
422
|
+
const decorators = (prop as any).decorators;
|
|
423
|
+
let isBody = false;
|
|
424
|
+
if (decorators) {
|
|
425
|
+
for (const key of Object.keys(decorators)) {
|
|
426
|
+
const decorator = decorators[key];
|
|
427
|
+
const name = getDecoratorName(decorator);
|
|
428
|
+
if (name === "body" || name === "bodyRoot") {
|
|
429
|
+
isBody = true;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (isBody) {
|
|
389
435
|
const { type: rustType } = getRustTypeForProperty(
|
|
390
436
|
prop.type,
|
|
391
437
|
program,
|
|
@@ -408,9 +454,10 @@ function getAllOperations(
|
|
|
408
454
|
namespace(ns: Namespace) {
|
|
409
455
|
const route = getRoute(program, ns);
|
|
410
456
|
if (!route) return;
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
457
|
+
const fullName = getNamespaceFullName(ns);
|
|
458
|
+
if (!seenNamespaces.has(fullName)) {
|
|
459
|
+
seenNamespaces.add(fullName);
|
|
460
|
+
namespaceOps.set(fullName, { ns, ops: [] });
|
|
414
461
|
}
|
|
415
462
|
},
|
|
416
463
|
operation(op: Operation) {
|
|
@@ -418,11 +465,12 @@ function getAllOperations(
|
|
|
418
465
|
if (!method) return;
|
|
419
466
|
const ns = op.namespace;
|
|
420
467
|
if (!ns) return;
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
468
|
+
const fullName = getNamespaceFullName(ns);
|
|
469
|
+
if (!seenNamespaces.has(fullName)) {
|
|
470
|
+
seenNamespaces.add(fullName);
|
|
471
|
+
namespaceOps.set(fullName, { ns, ops: [] });
|
|
424
472
|
}
|
|
425
|
-
const entry = namespaceOps.get(
|
|
473
|
+
const entry = namespaceOps.get(fullName);
|
|
426
474
|
if (entry) {
|
|
427
475
|
entry.ops.push(op);
|
|
428
476
|
}
|
|
@@ -670,7 +718,7 @@ function generateRouter(
|
|
|
670
718
|
const usedMethods = new Set<string>();
|
|
671
719
|
|
|
672
720
|
for (const group of namespaceGroups) {
|
|
673
|
-
const nsRoute =
|
|
721
|
+
const nsRoute = getFullRoute(program, group.namespace);
|
|
674
722
|
if (!nsRoute) continue;
|
|
675
723
|
|
|
676
724
|
const nsName = toPascalCase(
|
|
@@ -739,7 +787,7 @@ function generateRouter(
|
|
|
739
787
|
const serverCall = `service.${traitFnName}(${serverArgsStr}).await`;
|
|
740
788
|
|
|
741
789
|
// All handlers use <S> generics, Claims is now an associated type
|
|
742
|
-
|
|
790
|
+
const handlerCode = `pub async fn ${handlerFnName}_handler<S>(
|
|
743
791
|
axum::extract::State(service): axum::extract::State<S>,
|
|
744
792
|
${extractorLines.join("\n")}
|
|
745
793
|
) -> impl axum::response::IntoResponse
|
|
@@ -760,7 +808,7 @@ where
|
|
|
760
808
|
|
|
761
809
|
handlers.push(handlerCode);
|
|
762
810
|
|
|
763
|
-
|
|
811
|
+
const routePath = `"${opInfo.path}"`;
|
|
764
812
|
let routeStmt = "";
|
|
765
813
|
if (isProtected) {
|
|
766
814
|
routeStmt = `.route(${routePath}, ${method}(${handlerFnName}_handler::<S>))`;
|
|
@@ -796,14 +844,6 @@ ${routerBody}
|
|
|
796
844
|
return parts.join("\n");
|
|
797
845
|
}
|
|
798
846
|
|
|
799
|
-
function hasOnlyPathParams(
|
|
800
|
-
hasPath: boolean,
|
|
801
|
-
hasQuery: boolean,
|
|
802
|
-
hasBody: boolean,
|
|
803
|
-
): boolean {
|
|
804
|
-
return hasPath && !hasQuery && !hasBody;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
847
|
function buildRouterBody(
|
|
808
848
|
publicRoutes: string[],
|
|
809
849
|
protectedRoutes: string[],
|
|
@@ -1144,6 +1184,7 @@ function emitModel(
|
|
|
1144
1184
|
"serde::Deserialize",
|
|
1145
1185
|
];
|
|
1146
1186
|
|
|
1187
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1147
1188
|
const customDerives = (model as any)[rustDeriveKey] as
|
|
1148
1189
|
| RustDeriveInfo
|
|
1149
1190
|
| undefined;
|
|
@@ -1161,6 +1202,7 @@ function emitModel(
|
|
|
1161
1202
|
parts.push('#[error("{code}: {message}")]');
|
|
1162
1203
|
}
|
|
1163
1204
|
|
|
1205
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1164
1206
|
const customAttrs = (model as any)[rustAttrKey] as RustAttrInfo | undefined;
|
|
1165
1207
|
if (customAttrs) {
|
|
1166
1208
|
for (const attr of customAttrs.attrs) {
|
|
@@ -1245,6 +1287,7 @@ function emitEnum(enumType: Enum): string {
|
|
|
1245
1287
|
"serde::Deserialize",
|
|
1246
1288
|
];
|
|
1247
1289
|
|
|
1290
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1248
1291
|
const customDerives = (enumType as any)[rustDeriveKey] as
|
|
1249
1292
|
| RustDeriveInfo
|
|
1250
1293
|
| undefined;
|
|
@@ -1253,6 +1296,7 @@ function emitEnum(enumType: Enum): string {
|
|
|
1253
1296
|
allDerives.push(...customDerives.derives);
|
|
1254
1297
|
}
|
|
1255
1298
|
|
|
1299
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1256
1300
|
const customAttrs = (enumType as any)[rustAttrKey] as
|
|
1257
1301
|
| RustAttrInfo
|
|
1258
1302
|
| undefined;
|
|
@@ -1477,7 +1521,7 @@ export async function $onEmit(
|
|
|
1477
1521
|
|
|
1478
1522
|
const namespaceGroups = getAllOperations(context.program);
|
|
1479
1523
|
|
|
1480
|
-
const outputContent = content.join("\n");
|
|
1524
|
+
const outputContent = "#![allow(unused)]\n\n" + content.join("\n") + "\n";
|
|
1481
1525
|
|
|
1482
1526
|
await emitFile(context.program, {
|
|
1483
1527
|
path: resolvePath(context.emitterOutputDir, "types.rs"),
|
|
@@ -1501,7 +1545,10 @@ export async function $onEmit(
|
|
|
1501
1545
|
anonymousEnums,
|
|
1502
1546
|
);
|
|
1503
1547
|
|
|
1504
|
-
const serverContent =
|
|
1548
|
+
const serverContent =
|
|
1549
|
+
"#![allow(unused)]\n\n" +
|
|
1550
|
+
[serverTrait, responseEnums, router].join("\n") +
|
|
1551
|
+
"\n";
|
|
1505
1552
|
|
|
1506
1553
|
await emitFile(context.program, {
|
|
1507
1554
|
path: resolvePath(context.emitterOutputDir, "server.rs"),
|