typespec-rust-emitter 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +82 -80
- package/CHANGELOG.md +17 -0
- package/dist/src/decorators/cache_control.d.ts +6 -0
- package/dist/src/decorators/cache_control.js +9 -0
- package/dist/src/decorators/cache_control.js.map +1 -0
- package/dist/src/decorators/etag_cache.d.ts +6 -0
- package/dist/src/decorators/etag_cache.js +9 -0
- package/dist/src/decorators/etag_cache.js.map +1 -0
- package/dist/src/decorators/index.d.ts +6 -0
- package/dist/src/decorators/index.js +7 -0
- package/dist/src/decorators/index.js.map +1 -0
- package/dist/src/decorators/rust_attr.d.ts +3 -0
- package/dist/src/decorators/rust_attr.js +45 -0
- package/dist/src/decorators/rust_attr.js.map +1 -0
- package/dist/src/decorators/rust_derive.d.ts +3 -0
- package/dist/src/decorators/rust_derive.js +39 -0
- package/dist/src/decorators/rust_derive.js.map +1 -0
- package/dist/src/decorators/rust_impl.d.ts +2 -0
- package/dist/src/decorators/rust_impl.js +19 -0
- package/dist/src/decorators/rust_impl.js.map +1 -0
- package/dist/src/decorators/rust_self.d.ts +3 -0
- package/dist/src/decorators/rust_self.js +35 -0
- package/dist/src/decorators/rust_self.js.map +1 -0
- package/dist/src/emitter.d.ts +2 -11
- package/dist/src/emitter.js +7 -1282
- package/dist/src/emitter.js.map +1 -1
- package/dist/src/formatter/index.d.ts +2 -0
- package/dist/src/formatter/index.js +3 -0
- package/dist/src/formatter/index.js.map +1 -0
- package/dist/src/formatter/mappings.d.ts +4 -0
- package/dist/src/formatter/mappings.js +68 -0
- package/dist/src/formatter/mappings.js.map +1 -0
- package/dist/src/formatter/strings.d.ts +4 -0
- package/dist/src/formatter/strings.js +32 -0
- package/dist/src/formatter/strings.js.map +1 -0
- package/dist/src/generator/etag_router.d.ts +30 -0
- package/dist/src/generator/etag_router.js +123 -0
- package/dist/src/generator/etag_router.js.map +1 -0
- package/dist/src/generator/index.d.ts +5 -0
- package/dist/src/generator/index.js +6 -0
- package/dist/src/generator/index.js.map +1 -0
- package/dist/src/generator/response_enums.d.ts +6 -0
- package/dist/src/generator/response_enums.js +58 -0
- package/dist/src/generator/response_enums.js.map +1 -0
- package/dist/src/generator/router.d.ts +7 -0
- package/dist/src/generator/router.js +227 -0
- package/dist/src/generator/router.js.map +1 -0
- package/dist/src/generator/server_trait.d.ts +6 -0
- package/dist/src/generator/server_trait.js +97 -0
- package/dist/src/generator/server_trait.js.map +1 -0
- package/dist/src/generator/types_file.d.ts +11 -0
- package/dist/src/generator/types_file.js +209 -0
- package/dist/src/generator/types_file.js.map +1 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib.js +1 -1
- package/dist/src/lib.js.map +1 -1
- package/dist/src/models/index.d.ts +2 -0
- package/dist/src/models/index.js +3 -0
- package/dist/src/models/index.js.map +1 -0
- package/dist/src/models/keys.d.ts +6 -0
- package/dist/src/models/keys.js +8 -0
- package/dist/src/models/keys.js.map +1 -0
- package/dist/src/models/types.d.ts +45 -0
- package/dist/src/models/types.js +2 -0
- package/dist/src/models/types.js.map +1 -0
- package/dist/src/parser/decorators.d.ts +18 -0
- package/dist/src/parser/decorators.js +28 -0
- package/dist/src/parser/decorators.js.map +1 -0
- package/dist/src/parser/index.d.ts +6 -0
- package/dist/src/parser/index.js +7 -0
- package/dist/src/parser/index.js.map +1 -0
- package/dist/src/parser/operations.d.ts +13 -0
- package/dist/src/parser/operations.js +127 -0
- package/dist/src/parser/operations.js.map +1 -0
- package/dist/src/parser/parameters.d.ts +5 -0
- package/dist/src/parser/parameters.js +98 -0
- package/dist/src/parser/parameters.js.map +1 -0
- package/dist/src/parser/responses.d.ts +13 -0
- package/dist/src/parser/responses.js +132 -0
- package/dist/src/parser/responses.js.map +1 -0
- package/dist/src/parser/routes.d.ts +4 -0
- package/dist/src/parser/routes.js +36 -0
- package/dist/src/parser/routes.js.map +1 -0
- package/dist/src/parser/types.d.ts +9 -0
- package/dist/src/parser/types.js +157 -0
- package/dist/src/parser/types.js.map +1 -0
- package/dist/test/etag_cache.test.d.ts +1 -0
- package/dist/test/etag_cache.test.js +62 -0
- package/dist/test/etag_cache.test.js.map +1 -0
- package/dist/test/test-host.d.ts +11 -0
- package/dist/test/test-host.js +28 -0
- package/dist/test/test-host.js.map +1 -1
- package/example/main.tsp +27 -1
- package/example/output-rust/Cargo.lock +48 -0
- package/example/output-rust/Cargo.toml +1 -0
- package/example/output-rust/src/generated/server.rs +122 -11
- package/example/output-rust/src/generated/types.rs +6 -0
- package/example/output-rust/src/main.rs +60 -27
- package/justfile +31 -2
- package/package.json +1 -1
- package/scripts/update-golden.js +36 -0
- package/src/decorators/cache_control.ts +14 -0
- package/src/decorators/etag_cache.ts +14 -0
- package/src/decorators/index.ts +6 -0
- package/src/decorators/rust_attr.ts +61 -0
- package/src/decorators/rust_derive.ts +55 -0
- package/src/decorators/rust_impl.ts +29 -0
- package/src/decorators/rust_self.ts +42 -0
- package/src/emitter.ts +18 -1654
- package/src/formatter/index.ts +2 -0
- package/src/formatter/mappings.ts +70 -0
- package/src/formatter/strings.ts +33 -0
- package/src/generator/etag_router.ts +147 -0
- package/src/generator/index.ts +5 -0
- package/src/generator/response_enums.ts +76 -0
- package/src/generator/router.ts +280 -0
- package/src/generator/server_trait.ts +134 -0
- package/src/generator/types_file.ts +297 -0
- package/src/index.ts +3 -1
- package/src/lib.ts +1 -1
- package/src/lib.tsp +3 -1
- package/src/models/index.ts +2 -0
- package/src/models/keys.ts +7 -0
- package/src/models/types.ts +54 -0
- package/src/parser/decorators.ts +34 -0
- package/src/parser/index.ts +6 -0
- package/src/parser/operations.ts +158 -0
- package/src/parser/parameters.ts +117 -0
- package/src/parser/responses.ts +170 -0
- package/src/parser/routes.ts +47 -0
- package/src/parser/types.ts +215 -0
- package/test/etag_cache.test.ts +69 -0
- package/test/golden/etag_cache/server.rs +109 -0
- package/test/golden/etag_cache/spec.tsp +20 -0
- package/test/golden/etag_cache/types.rs +13 -0
- package/test/test-host.ts +43 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDoc,
|
|
3
|
+
getNamespaceFullName,
|
|
4
|
+
getTags,
|
|
5
|
+
Namespace,
|
|
6
|
+
navigateProgram,
|
|
7
|
+
Operation,
|
|
8
|
+
Program,
|
|
9
|
+
} from "@typespec/compiler";
|
|
10
|
+
import {
|
|
11
|
+
AnonymousStringLiteralUnion,
|
|
12
|
+
OperationInfo,
|
|
13
|
+
SelfReceiver,
|
|
14
|
+
} from "../models/types.js";
|
|
15
|
+
import {
|
|
16
|
+
cacheControlKey,
|
|
17
|
+
etagCacheKey,
|
|
18
|
+
rustSelfReceiverKey,
|
|
19
|
+
} from "../models/keys.js";
|
|
20
|
+
import { getDecoratorName } from "./decorators.js";
|
|
21
|
+
import { buildFullPath, getRoute } from "./routes.js";
|
|
22
|
+
import { getOperationParameters, getOperationBody } from "./parameters.js";
|
|
23
|
+
import { getOperationResponses } from "./responses.js";
|
|
24
|
+
|
|
25
|
+
export function hasAuthDecorator(operation: Operation): boolean {
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
const decorators = (operation as any).decorators;
|
|
28
|
+
if (!decorators) return false;
|
|
29
|
+
|
|
30
|
+
for (const key of Object.keys(decorators)) {
|
|
31
|
+
const decorator = decorators[key];
|
|
32
|
+
const name = getDecoratorName(decorator);
|
|
33
|
+
if (name === "useAuth") {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getSelfReceiver(operation: Operation): SelfReceiver {
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
const receiver = (operation as any)[rustSelfReceiverKey] as
|
|
43
|
+
| SelfReceiver
|
|
44
|
+
| undefined;
|
|
45
|
+
return receiver ?? "&self";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getHttpMethod(
|
|
49
|
+
_program: Program,
|
|
50
|
+
operation: Operation,
|
|
51
|
+
): "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | undefined {
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
+
const decorators = (operation as any).decorators;
|
|
54
|
+
if (!decorators) return undefined;
|
|
55
|
+
|
|
56
|
+
for (const key of Object.keys(decorators)) {
|
|
57
|
+
const decorator = decorators[key];
|
|
58
|
+
const name = getDecoratorName(decorator);
|
|
59
|
+
if (name === "get") return "GET";
|
|
60
|
+
if (name === "post") return "POST";
|
|
61
|
+
if (name === "put") return "PUT";
|
|
62
|
+
if (name === "patch") return "PATCH";
|
|
63
|
+
if (name === "delete") return "DELETE";
|
|
64
|
+
if (name === "head") return "HEAD";
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getAllOperations(
|
|
70
|
+
program: Program,
|
|
71
|
+
): { namespace: Namespace; operations: Operation[] }[] {
|
|
72
|
+
const seenNamespaces = new Set<string>();
|
|
73
|
+
const namespaceOps = new Map<string, { ns: Namespace; ops: Operation[] }>();
|
|
74
|
+
|
|
75
|
+
navigateProgram(program, {
|
|
76
|
+
namespace(ns: Namespace) {
|
|
77
|
+
const route = getRoute(program, ns);
|
|
78
|
+
if (!route) return;
|
|
79
|
+
const fullName = getNamespaceFullName(ns);
|
|
80
|
+
if (!seenNamespaces.has(fullName)) {
|
|
81
|
+
seenNamespaces.add(fullName);
|
|
82
|
+
namespaceOps.set(fullName, { ns, ops: [] });
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
operation(op: Operation) {
|
|
86
|
+
const method = getHttpMethod(program, op);
|
|
87
|
+
if (!method) return;
|
|
88
|
+
const ns = op.namespace;
|
|
89
|
+
if (!ns) return;
|
|
90
|
+
const fullName = getNamespaceFullName(ns);
|
|
91
|
+
if (!seenNamespaces.has(fullName)) {
|
|
92
|
+
seenNamespaces.add(fullName);
|
|
93
|
+
namespaceOps.set(fullName, { ns, ops: [] });
|
|
94
|
+
}
|
|
95
|
+
const entry = namespaceOps.get(fullName);
|
|
96
|
+
if (entry) {
|
|
97
|
+
entry.ops.push(op);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const result: { namespace: Namespace; operations: Operation[] }[] = [];
|
|
103
|
+
for (const [, entry] of namespaceOps) {
|
|
104
|
+
if (entry.ops.length > 0) {
|
|
105
|
+
result.push({ namespace: entry.ns, operations: entry.ops });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function emitOperationInfo(
|
|
112
|
+
program: Program,
|
|
113
|
+
op: Operation,
|
|
114
|
+
nsRoute: string,
|
|
115
|
+
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
116
|
+
): OperationInfo | undefined {
|
|
117
|
+
const method = getHttpMethod(program, op);
|
|
118
|
+
if (!method) return undefined;
|
|
119
|
+
|
|
120
|
+
const opRoute = getRoute(program, op);
|
|
121
|
+
const fullPath = buildFullPath(nsRoute, opRoute);
|
|
122
|
+
const tags = getTags(program, op) ?? [];
|
|
123
|
+
const params = getOperationParameters(program, op, anonymousEnums);
|
|
124
|
+
const body = getOperationBody(op);
|
|
125
|
+
const responses = getOperationResponses(program, op, anonymousEnums);
|
|
126
|
+
const doc = getDoc(program, op);
|
|
127
|
+
|
|
128
|
+
const opName = op.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
129
|
+
const etagVal = program.stateMap(etagCacheKey).get(op);
|
|
130
|
+
const etagKey = typeof etagVal === "string" ? etagVal : undefined;
|
|
131
|
+
const cacheControl = program.stateMap(cacheControlKey).get(op) as string | undefined;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
name: opName,
|
|
135
|
+
method,
|
|
136
|
+
path: fullPath,
|
|
137
|
+
tags,
|
|
138
|
+
parameters: params,
|
|
139
|
+
body: body,
|
|
140
|
+
responses,
|
|
141
|
+
doc,
|
|
142
|
+
etagKey,
|
|
143
|
+
cacheControl,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function getEtagKey(program: Program, operation: Operation): string | undefined {
|
|
148
|
+
const val = program.stateMap(etagCacheKey).get(operation);
|
|
149
|
+
return typeof val === "string" ? val : undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function hasEtagCache(program: Program, operation: Operation): boolean {
|
|
153
|
+
return program.stateMap(etagCacheKey).has(operation);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function getCacheControl(program: Program, operation: Operation): string | undefined {
|
|
157
|
+
return program.stateMap(cacheControlKey).get(operation) as string | undefined;
|
|
158
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { ModelProperty, Operation, Program } from "@typespec/compiler";
|
|
2
|
+
import { AnonymousStringLiteralUnion, ParameterInfo } from "../models/types.js";
|
|
3
|
+
import { toRustIdent } from "../formatter/strings.js";
|
|
4
|
+
import { getDecoratorArgValue, getDecoratorName } from "./decorators.js";
|
|
5
|
+
import { getRustTypeForProperty } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export function getOperationParameters(
|
|
8
|
+
program: Program,
|
|
9
|
+
operation: Operation,
|
|
10
|
+
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
11
|
+
): ParameterInfo[] {
|
|
12
|
+
const params: ParameterInfo[] = [];
|
|
13
|
+
const model = operation.parameters;
|
|
14
|
+
|
|
15
|
+
for (const [propName, prop] of model.properties) {
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
const decorators = (prop as any).decorators;
|
|
18
|
+
|
|
19
|
+
// Skip body parameters - they are handled separately
|
|
20
|
+
let isBody = false;
|
|
21
|
+
if (decorators) {
|
|
22
|
+
for (const key of Object.keys(decorators)) {
|
|
23
|
+
const decorator = decorators[key];
|
|
24
|
+
const name = getDecoratorName(decorator);
|
|
25
|
+
if (
|
|
26
|
+
name === "body" ||
|
|
27
|
+
name === "bodyRoot" ||
|
|
28
|
+
name === "multipartBody"
|
|
29
|
+
) {
|
|
30
|
+
isBody = true;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (isBody) continue;
|
|
36
|
+
|
|
37
|
+
let location: "path" | "query" | "header" | "cookie" = "query";
|
|
38
|
+
let rustName = toRustIdent(propName);
|
|
39
|
+
|
|
40
|
+
if (decorators) {
|
|
41
|
+
for (const key of Object.keys(decorators)) {
|
|
42
|
+
const decorator = decorators[key];
|
|
43
|
+
const name = getDecoratorName(decorator);
|
|
44
|
+
if (name === "path") {
|
|
45
|
+
location = "path";
|
|
46
|
+
break;
|
|
47
|
+
} else if (name === "query") {
|
|
48
|
+
location = "query";
|
|
49
|
+
break;
|
|
50
|
+
} else if (name === "header") {
|
|
51
|
+
location = "header";
|
|
52
|
+
const headerVal = getDecoratorArgValue(decorator, 0);
|
|
53
|
+
if (headerVal) {
|
|
54
|
+
rustName = toRustIdent(headerVal);
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
} else if (name === "cookie") {
|
|
58
|
+
location = "cookie";
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { type: rustType } = getRustTypeForProperty(
|
|
65
|
+
prop.type,
|
|
66
|
+
program,
|
|
67
|
+
anonymousEnums,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
params.push({
|
|
71
|
+
name: propName,
|
|
72
|
+
rustName,
|
|
73
|
+
rustType,
|
|
74
|
+
location,
|
|
75
|
+
required: !prop.optional,
|
|
76
|
+
optional: prop.optional,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return params;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getOperationBody(
|
|
84
|
+
operation: Operation,
|
|
85
|
+
): ModelProperty | undefined {
|
|
86
|
+
for (const [_propName, prop] of operation.parameters.properties) {
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
+
const decorators = (prop as any).decorators;
|
|
89
|
+
if (!decorators) continue;
|
|
90
|
+
|
|
91
|
+
for (const key of Object.keys(decorators)) {
|
|
92
|
+
const decorator = decorators[key];
|
|
93
|
+
const name = getDecoratorName(decorator);
|
|
94
|
+
if (name === "body" || name === "bodyRoot" || name === "multipartBody") {
|
|
95
|
+
return prop;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function hasMultipartBody(operation: Operation): boolean {
|
|
103
|
+
for (const [_propName, prop] of operation.parameters.properties) {
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
+
const decorators = (prop as any).decorators;
|
|
106
|
+
if (!decorators) continue;
|
|
107
|
+
|
|
108
|
+
for (const key of Object.keys(decorators)) {
|
|
109
|
+
const decorator = decorators[key];
|
|
110
|
+
const name = getDecoratorName(decorator);
|
|
111
|
+
if (name === "multipartBody") {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDoc,
|
|
3
|
+
Model,
|
|
4
|
+
Operation,
|
|
5
|
+
Program,
|
|
6
|
+
Type,
|
|
7
|
+
Union,
|
|
8
|
+
} from "@typespec/compiler";
|
|
9
|
+
import { AnonymousStringLiteralUnion, ResponseInfo } from "../models/types.js";
|
|
10
|
+
import { getDecoratorName } from "./decorators.js";
|
|
11
|
+
import { getRustTypeForProperty } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export function getOperationResponses(
|
|
14
|
+
program: Program,
|
|
15
|
+
operation: Operation,
|
|
16
|
+
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
17
|
+
): ResponseInfo[] {
|
|
18
|
+
const responses: ResponseInfo[] = [];
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
const returnType = operation.returnType as any;
|
|
21
|
+
|
|
22
|
+
// Check for Array return type first (use runtime check since TS doesn't narrow Array)
|
|
23
|
+
if (
|
|
24
|
+
returnType.kind !== "Union" &&
|
|
25
|
+
returnType.kind !== "Model" &&
|
|
26
|
+
returnType.valueType
|
|
27
|
+
) {
|
|
28
|
+
// This is an Array type
|
|
29
|
+
const { type: rustType } = getRustTypeForProperty(
|
|
30
|
+
returnType.valueType,
|
|
31
|
+
program,
|
|
32
|
+
anonymousEnums,
|
|
33
|
+
);
|
|
34
|
+
responses.push({
|
|
35
|
+
statusCode: 200,
|
|
36
|
+
bodyType: rustType,
|
|
37
|
+
bodyDescription: "",
|
|
38
|
+
});
|
|
39
|
+
return responses;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (returnType.kind === "Union") {
|
|
43
|
+
const union = returnType as Union;
|
|
44
|
+
for (const variant of union.variants.values()) {
|
|
45
|
+
const statusCode = getStatusCode(variant);
|
|
46
|
+
const bodyInfo = getBodyFromResponse(variant, program, anonymousEnums);
|
|
47
|
+
responses.push({
|
|
48
|
+
statusCode,
|
|
49
|
+
bodyType: bodyInfo.type,
|
|
50
|
+
bodyDescription: bodyInfo.description,
|
|
51
|
+
isSse: bodyInfo.isSse,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
} else if (returnType.kind === "Model") {
|
|
55
|
+
const model = returnType as Model;
|
|
56
|
+
if (model.name === "SSEStream") {
|
|
57
|
+
responses.push({
|
|
58
|
+
statusCode: 200,
|
|
59
|
+
bodyType: "axum::response::Response",
|
|
60
|
+
bodyDescription: "Server-Sent Events stream",
|
|
61
|
+
isSse: true,
|
|
62
|
+
});
|
|
63
|
+
return responses;
|
|
64
|
+
}
|
|
65
|
+
let statusCode = 200;
|
|
66
|
+
let bodyType: string | undefined;
|
|
67
|
+
let bodyDescription: string | undefined;
|
|
68
|
+
const isSse = false;
|
|
69
|
+
|
|
70
|
+
for (const [propName, prop] of model.properties) {
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
72
|
+
const decorators = (prop as any).decorators;
|
|
73
|
+
let isBody = false;
|
|
74
|
+
if (decorators) {
|
|
75
|
+
for (const key of Object.keys(decorators)) {
|
|
76
|
+
const decorator = decorators[key];
|
|
77
|
+
const name = getDecoratorName(decorator);
|
|
78
|
+
if (name === "body" || name === "bodyRoot") {
|
|
79
|
+
isBody = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (isBody || propName === "body") {
|
|
85
|
+
const { type: rustType } = getRustTypeForProperty(
|
|
86
|
+
prop.type,
|
|
87
|
+
program,
|
|
88
|
+
anonymousEnums,
|
|
89
|
+
);
|
|
90
|
+
bodyType = rustType;
|
|
91
|
+
bodyDescription = getDoc(program, prop);
|
|
92
|
+
} else if (propName === "statusCode") {
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
|
+
const typeAny = prop.type as any;
|
|
95
|
+
if (typeAny.value !== undefined) {
|
|
96
|
+
statusCode = typeAny.value as number;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
responses.push({
|
|
101
|
+
statusCode,
|
|
102
|
+
bodyType,
|
|
103
|
+
bodyDescription,
|
|
104
|
+
isSse,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return responses;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getStatusCode(variant: { type: Type }): number {
|
|
112
|
+
if (variant.type.kind === "Model") {
|
|
113
|
+
const model = variant.type as Model;
|
|
114
|
+
for (const [propName, prop] of model.properties) {
|
|
115
|
+
if (propName === "statusCode") {
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
117
|
+
const typeAny = prop.type as any;
|
|
118
|
+
if (typeAny.value !== undefined) {
|
|
119
|
+
return typeAny.value as number;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return 200;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function getBodyFromResponse(
|
|
128
|
+
variant: { type: Type },
|
|
129
|
+
program: Program,
|
|
130
|
+
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
131
|
+
): {
|
|
132
|
+
type: string | undefined;
|
|
133
|
+
description: string | undefined;
|
|
134
|
+
isSse?: boolean;
|
|
135
|
+
} {
|
|
136
|
+
if (variant.type.kind === "Model") {
|
|
137
|
+
const model = variant.type as Model;
|
|
138
|
+
if (model.name === "SSEStream") {
|
|
139
|
+
return {
|
|
140
|
+
type: "axum::response::Response",
|
|
141
|
+
description: "Server-Sent Events stream",
|
|
142
|
+
isSse: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
for (const [_propName, prop] of model.properties) {
|
|
146
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
147
|
+
const decorators = (prop as any).decorators;
|
|
148
|
+
let isBody = false;
|
|
149
|
+
if (decorators) {
|
|
150
|
+
for (const key of Object.keys(decorators)) {
|
|
151
|
+
const decorator = decorators[key];
|
|
152
|
+
const name = getDecoratorName(decorator);
|
|
153
|
+
if (name === "body" || name === "bodyRoot") {
|
|
154
|
+
isBody = true;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (isBody) {
|
|
160
|
+
const { type: rustType } = getRustTypeForProperty(
|
|
161
|
+
prop.type,
|
|
162
|
+
program,
|
|
163
|
+
anonymousEnums,
|
|
164
|
+
);
|
|
165
|
+
return { type: rustType, description: getDoc(program, prop) };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { type: undefined, description: undefined };
|
|
170
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Namespace, Operation, Program } from "@typespec/compiler";
|
|
2
|
+
import { getDecoratorName, getDecoratorArgValue } from "./decorators.js";
|
|
3
|
+
|
|
4
|
+
export function getRoute(
|
|
5
|
+
_program: Program,
|
|
6
|
+
target: Namespace | Operation,
|
|
7
|
+
): string {
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
const decorators = (target as any).decorators;
|
|
10
|
+
if (!decorators) return "";
|
|
11
|
+
|
|
12
|
+
for (const key of Object.keys(decorators)) {
|
|
13
|
+
const decorator = decorators[key];
|
|
14
|
+
const name = getDecoratorName(decorator);
|
|
15
|
+
if (name === "route") {
|
|
16
|
+
return getDecoratorArgValue(decorator, 0);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getFullRoute(program: Program, ns: Namespace): string {
|
|
23
|
+
const routes: string[] = [];
|
|
24
|
+
let current: Namespace | undefined = ns;
|
|
25
|
+
|
|
26
|
+
while (current) {
|
|
27
|
+
const route = getRoute(program, current);
|
|
28
|
+
if (route) {
|
|
29
|
+
routes.unshift(route);
|
|
30
|
+
}
|
|
31
|
+
current = current.namespace;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return routes.join("");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildFullPath(
|
|
38
|
+
namespaceRoute: string,
|
|
39
|
+
operationRoute: string,
|
|
40
|
+
): string {
|
|
41
|
+
let fullPath = namespaceRoute + operationRoute;
|
|
42
|
+
fullPath = fullPath.replace(/\/+/g, "/");
|
|
43
|
+
if (fullPath.length > 1 && fullPath.endsWith("/")) {
|
|
44
|
+
fullPath = fullPath.slice(0, -1);
|
|
45
|
+
}
|
|
46
|
+
return fullPath;
|
|
47
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Enum,
|
|
3
|
+
getFormat,
|
|
4
|
+
getPattern,
|
|
5
|
+
IntrinsicType,
|
|
6
|
+
isArrayModelType,
|
|
7
|
+
isRecordModelType,
|
|
8
|
+
Model,
|
|
9
|
+
ModelProperty,
|
|
10
|
+
Namespace,
|
|
11
|
+
Program,
|
|
12
|
+
Scalar,
|
|
13
|
+
StringLiteral,
|
|
14
|
+
Type,
|
|
15
|
+
Union,
|
|
16
|
+
} from "@typespec/compiler";
|
|
17
|
+
import { formatToRust, scalarToRust } from "../formatter/mappings.js";
|
|
18
|
+
import { toPascalCase } from "../formatter/strings.js";
|
|
19
|
+
import { AnonymousStringLiteralUnion } from "../models/types.js";
|
|
20
|
+
|
|
21
|
+
const typeSpecNamespaces = new Set([
|
|
22
|
+
"TypeSpec",
|
|
23
|
+
"@typespec/http",
|
|
24
|
+
"@typespec/rest",
|
|
25
|
+
"@typespec/openapi",
|
|
26
|
+
"@typespec/openapi3",
|
|
27
|
+
"@typespec/json-schema",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export function isStdLibNamespace(ns: Namespace | undefined): boolean {
|
|
31
|
+
if (!ns) return false;
|
|
32
|
+
const fullName = ns.name;
|
|
33
|
+
if (typeSpecNamespaces.has(fullName)) return true;
|
|
34
|
+
if (ns.namespace) return isStdLibNamespace(ns.namespace);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isStdLibType(type: Type): boolean {
|
|
39
|
+
if ("namespace" in type) {
|
|
40
|
+
const ns = (type as Model | Enum | Union | Scalar).namespace;
|
|
41
|
+
if (isStdLibNamespace(ns as Namespace)) return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getRustTypeForProperty(
|
|
47
|
+
type: Type,
|
|
48
|
+
program: Program,
|
|
49
|
+
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
50
|
+
): { type: string; isStringLiteral: boolean; stringLiteralValue?: string } {
|
|
51
|
+
const kind = type.kind as string;
|
|
52
|
+
|
|
53
|
+
if (kind === "Model") {
|
|
54
|
+
const model = type as Model;
|
|
55
|
+
if (isArrayModelType(model) && model.indexer?.value) {
|
|
56
|
+
const element = getRustTypeForProperty(
|
|
57
|
+
model.indexer.value,
|
|
58
|
+
program,
|
|
59
|
+
anonymousEnums,
|
|
60
|
+
);
|
|
61
|
+
return { type: `Vec<${element.type}>`, isStringLiteral: false };
|
|
62
|
+
}
|
|
63
|
+
if (isRecordModelType(model) && model.indexer?.value) {
|
|
64
|
+
const value = getRustTypeForProperty(
|
|
65
|
+
model.indexer.value,
|
|
66
|
+
program,
|
|
67
|
+
anonymousEnums,
|
|
68
|
+
);
|
|
69
|
+
return {
|
|
70
|
+
type: `std::collections::HashMap<String, ${value.type}>`,
|
|
71
|
+
isStringLiteral: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return { type: toPascalCase(model.name), isStringLiteral: false };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (kind === "ModelProperty") {
|
|
78
|
+
return getRustTypeForProperty(
|
|
79
|
+
(type as ModelProperty).type,
|
|
80
|
+
program,
|
|
81
|
+
anonymousEnums,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (kind === "Enum") {
|
|
86
|
+
return { type: toPascalCase((type as Enum).name), isStringLiteral: false };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (kind === "Union") {
|
|
90
|
+
const unionType = type as Union;
|
|
91
|
+
const variants = Array.from(unionType.variants.values());
|
|
92
|
+
const allStringLiterals = variants.every((v) => v.type.kind === "String");
|
|
93
|
+
|
|
94
|
+
if (allStringLiterals) {
|
|
95
|
+
if (unionType.name) {
|
|
96
|
+
return {
|
|
97
|
+
type: toPascalCase(unionType.name),
|
|
98
|
+
isStringLiteral: false,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const values = variants.map((v) => (v.type as StringLiteral).value);
|
|
102
|
+
const sanitized = values.map((v) => v.replace(/_/g, ""));
|
|
103
|
+
const firstTwo = sanitized.slice(0, 2).map(toPascalCase).join("");
|
|
104
|
+
const enumName = `Enum${firstTwo}${sanitized.length}`;
|
|
105
|
+
if (!anonymousEnums.has(enumName)) {
|
|
106
|
+
anonymousEnums.set(enumName, {
|
|
107
|
+
enumName,
|
|
108
|
+
variants: variants.map((v) => v.type as StringLiteral),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return { type: enumName, isStringLiteral: false };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const nonNullVariants = variants.filter((v) => {
|
|
115
|
+
const vt = v.type;
|
|
116
|
+
if ((vt.kind as string) === "Null") return false;
|
|
117
|
+
if (
|
|
118
|
+
(vt.kind as string) === "Intrinsic" &&
|
|
119
|
+
(vt as IntrinsicType).name === "null"
|
|
120
|
+
)
|
|
121
|
+
return false;
|
|
122
|
+
return true;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (
|
|
126
|
+
nonNullVariants.length === 1 &&
|
|
127
|
+
(variants.length === 2 ||
|
|
128
|
+
variants.some((v) => {
|
|
129
|
+
const vt = v.type;
|
|
130
|
+
return (
|
|
131
|
+
(vt.kind as string) === "Null" ||
|
|
132
|
+
((vt.kind as string) === "Intrinsic" &&
|
|
133
|
+
(vt as IntrinsicType).name === "null")
|
|
134
|
+
);
|
|
135
|
+
}))
|
|
136
|
+
) {
|
|
137
|
+
const vt = getRustTypeForProperty(
|
|
138
|
+
nonNullVariants[0].type,
|
|
139
|
+
program,
|
|
140
|
+
anonymousEnums,
|
|
141
|
+
);
|
|
142
|
+
return { type: `Option<${vt.type}>`, isStringLiteral: false };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const variantStrings: string[] = [];
|
|
146
|
+
for (const v of variants) {
|
|
147
|
+
const vt = v.type;
|
|
148
|
+
if ((vt.kind as string) === "Null") continue;
|
|
149
|
+
if (
|
|
150
|
+
(vt.kind as string) === "Intrinsic" &&
|
|
151
|
+
(vt as IntrinsicType).name === "null"
|
|
152
|
+
)
|
|
153
|
+
continue;
|
|
154
|
+
const rustVt = getRustTypeForProperty(vt, program, anonymousEnums);
|
|
155
|
+
variantStrings.push(rustVt.type);
|
|
156
|
+
}
|
|
157
|
+
const uniqueTypes = [...new Set(variantStrings)];
|
|
158
|
+
const resultType =
|
|
159
|
+
uniqueTypes.length === 1
|
|
160
|
+
? uniqueTypes[0]
|
|
161
|
+
: `(${uniqueTypes.join(" | ")})`;
|
|
162
|
+
return { type: resultType, isStringLiteral: false };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (kind === "Scalar") {
|
|
166
|
+
const scalar = type as Scalar;
|
|
167
|
+
const format = getFormat(program, scalar);
|
|
168
|
+
const pattern = getPattern(program, scalar);
|
|
169
|
+
|
|
170
|
+
if (format && formatToRust[format] && !pattern) {
|
|
171
|
+
return { type: formatToRust[format], isStringLiteral: false };
|
|
172
|
+
}
|
|
173
|
+
if (pattern) {
|
|
174
|
+
return { type: toPascalCase(scalar.name), isStringLiteral: false };
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
type: scalarToRust[scalar.name] ?? scalar.name,
|
|
178
|
+
isStringLiteral: false,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (kind === "Intrinsic") {
|
|
183
|
+
const intrinsic = type as IntrinsicType;
|
|
184
|
+
const format = getFormat(program, type);
|
|
185
|
+
if (format && formatToRust[format]) {
|
|
186
|
+
return { type: formatToRust[format], isStringLiteral: false };
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
type: scalarToRust[intrinsic.name] ?? "serde_json::Value",
|
|
190
|
+
isStringLiteral: false,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (kind === "String") {
|
|
195
|
+
return { type: "String", isStringLiteral: false };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (kind === "StringLiteral") {
|
|
199
|
+
return {
|
|
200
|
+
type: "String",
|
|
201
|
+
isStringLiteral: true,
|
|
202
|
+
stringLiteralValue: (type as StringLiteral).value,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (kind === "Boolean" || kind === "BooleanLiteral") {
|
|
207
|
+
return { type: "bool", isStringLiteral: false };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (kind === "Number" || kind === "NumericLiteral") {
|
|
211
|
+
return { type: "f64", isStringLiteral: false };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { type: "serde_json::Value", isStringLiteral: false };
|
|
215
|
+
}
|