typespec-rust-emitter 0.12.0 → 0.13.1

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.
Files changed (138) hide show
  1. package/AGENTS.md +82 -80
  2. package/CHANGELOG.md +24 -0
  3. package/dist/src/decorators/cache_control.d.ts +6 -0
  4. package/dist/src/decorators/cache_control.js +9 -0
  5. package/dist/src/decorators/cache_control.js.map +1 -0
  6. package/dist/src/decorators/etag_cache.d.ts +6 -0
  7. package/dist/src/decorators/etag_cache.js +9 -0
  8. package/dist/src/decorators/etag_cache.js.map +1 -0
  9. package/dist/src/decorators/index.d.ts +6 -0
  10. package/dist/src/decorators/index.js +7 -0
  11. package/dist/src/decorators/index.js.map +1 -0
  12. package/dist/src/decorators/rust_attr.d.ts +3 -0
  13. package/dist/src/decorators/rust_attr.js +45 -0
  14. package/dist/src/decorators/rust_attr.js.map +1 -0
  15. package/dist/src/decorators/rust_derive.d.ts +3 -0
  16. package/dist/src/decorators/rust_derive.js +39 -0
  17. package/dist/src/decorators/rust_derive.js.map +1 -0
  18. package/dist/src/decorators/rust_impl.d.ts +2 -0
  19. package/dist/src/decorators/rust_impl.js +19 -0
  20. package/dist/src/decorators/rust_impl.js.map +1 -0
  21. package/dist/src/decorators/rust_self.d.ts +3 -0
  22. package/dist/src/decorators/rust_self.js +35 -0
  23. package/dist/src/decorators/rust_self.js.map +1 -0
  24. package/dist/src/emitter.d.ts +2 -11
  25. package/dist/src/emitter.js +7 -1282
  26. package/dist/src/emitter.js.map +1 -1
  27. package/dist/src/formatter/index.d.ts +2 -0
  28. package/dist/src/formatter/index.js +3 -0
  29. package/dist/src/formatter/index.js.map +1 -0
  30. package/dist/src/formatter/mappings.d.ts +4 -0
  31. package/dist/src/formatter/mappings.js +68 -0
  32. package/dist/src/formatter/mappings.js.map +1 -0
  33. package/dist/src/formatter/strings.d.ts +4 -0
  34. package/dist/src/formatter/strings.js +32 -0
  35. package/dist/src/formatter/strings.js.map +1 -0
  36. package/dist/src/generator/etag_router.d.ts +28 -0
  37. package/dist/src/generator/etag_router.js +76 -0
  38. package/dist/src/generator/etag_router.js.map +1 -0
  39. package/dist/src/generator/index.d.ts +5 -0
  40. package/dist/src/generator/index.js +6 -0
  41. package/dist/src/generator/index.js.map +1 -0
  42. package/dist/src/generator/response_enums.d.ts +6 -0
  43. package/dist/src/generator/response_enums.js +58 -0
  44. package/dist/src/generator/response_enums.js.map +1 -0
  45. package/dist/src/generator/router.d.ts +7 -0
  46. package/dist/src/generator/router.js +231 -0
  47. package/dist/src/generator/router.js.map +1 -0
  48. package/dist/src/generator/server_trait.d.ts +6 -0
  49. package/dist/src/generator/server_trait.js +97 -0
  50. package/dist/src/generator/server_trait.js.map +1 -0
  51. package/dist/src/generator/types_file.d.ts +11 -0
  52. package/dist/src/generator/types_file.js +209 -0
  53. package/dist/src/generator/types_file.js.map +1 -0
  54. package/dist/src/index.d.ts +1 -1
  55. package/dist/src/index.js +1 -1
  56. package/dist/src/index.js.map +1 -1
  57. package/dist/src/lib.js +1 -1
  58. package/dist/src/lib.js.map +1 -1
  59. package/dist/src/models/index.d.ts +2 -0
  60. package/dist/src/models/index.js +3 -0
  61. package/dist/src/models/index.js.map +1 -0
  62. package/dist/src/models/keys.d.ts +6 -0
  63. package/dist/src/models/keys.js +8 -0
  64. package/dist/src/models/keys.js.map +1 -0
  65. package/dist/src/models/types.d.ts +45 -0
  66. package/dist/src/models/types.js +2 -0
  67. package/dist/src/models/types.js.map +1 -0
  68. package/dist/src/parser/decorators.d.ts +18 -0
  69. package/dist/src/parser/decorators.js +28 -0
  70. package/dist/src/parser/decorators.js.map +1 -0
  71. package/dist/src/parser/index.d.ts +6 -0
  72. package/dist/src/parser/index.js +7 -0
  73. package/dist/src/parser/index.js.map +1 -0
  74. package/dist/src/parser/operations.d.ts +13 -0
  75. package/dist/src/parser/operations.js +127 -0
  76. package/dist/src/parser/operations.js.map +1 -0
  77. package/dist/src/parser/parameters.d.ts +5 -0
  78. package/dist/src/parser/parameters.js +98 -0
  79. package/dist/src/parser/parameters.js.map +1 -0
  80. package/dist/src/parser/responses.d.ts +13 -0
  81. package/dist/src/parser/responses.js +132 -0
  82. package/dist/src/parser/responses.js.map +1 -0
  83. package/dist/src/parser/routes.d.ts +4 -0
  84. package/dist/src/parser/routes.js +36 -0
  85. package/dist/src/parser/routes.js.map +1 -0
  86. package/dist/src/parser/types.d.ts +9 -0
  87. package/dist/src/parser/types.js +157 -0
  88. package/dist/src/parser/types.js.map +1 -0
  89. package/dist/test/etag_cache.test.d.ts +1 -0
  90. package/dist/test/etag_cache.test.js +86 -0
  91. package/dist/test/etag_cache.test.js.map +1 -0
  92. package/dist/test/test-host.d.ts +11 -0
  93. package/dist/test/test-host.js +28 -0
  94. package/dist/test/test-host.js.map +1 -1
  95. package/example/main.tsp +30 -1
  96. package/example/output-rust/Cargo.lock +48 -0
  97. package/example/output-rust/Cargo.toml +1 -0
  98. package/example/output-rust/src/generated/server.rs +153 -12
  99. package/example/output-rust/src/generated/types.rs +6 -0
  100. package/example/output-rust/src/main.rs +65 -27
  101. package/justfile +31 -2
  102. package/package.json +1 -1
  103. package/scripts/update-golden.js +36 -0
  104. package/src/decorators/cache_control.ts +14 -0
  105. package/src/decorators/etag_cache.ts +14 -0
  106. package/src/decorators/index.ts +6 -0
  107. package/src/decorators/rust_attr.ts +61 -0
  108. package/src/decorators/rust_derive.ts +55 -0
  109. package/src/decorators/rust_impl.ts +29 -0
  110. package/src/decorators/rust_self.ts +42 -0
  111. package/src/emitter.ts +18 -1654
  112. package/src/formatter/index.ts +2 -0
  113. package/src/formatter/mappings.ts +70 -0
  114. package/src/formatter/strings.ts +33 -0
  115. package/src/generator/etag_router.ts +97 -0
  116. package/src/generator/index.ts +5 -0
  117. package/src/generator/response_enums.ts +76 -0
  118. package/src/generator/router.ts +284 -0
  119. package/src/generator/server_trait.ts +134 -0
  120. package/src/generator/types_file.ts +297 -0
  121. package/src/index.ts +3 -1
  122. package/src/lib.ts +1 -1
  123. package/src/lib.tsp +3 -1
  124. package/src/models/index.ts +2 -0
  125. package/src/models/keys.ts +7 -0
  126. package/src/models/types.ts +54 -0
  127. package/src/parser/decorators.ts +34 -0
  128. package/src/parser/index.ts +6 -0
  129. package/src/parser/operations.ts +158 -0
  130. package/src/parser/parameters.ts +117 -0
  131. package/src/parser/responses.ts +170 -0
  132. package/src/parser/routes.ts +47 -0
  133. package/src/parser/types.ts +215 -0
  134. package/test/etag_cache.test.ts +104 -0
  135. package/test/golden/etag_cache/server.rs +110 -0
  136. package/test/golden/etag_cache/spec.tsp +20 -0
  137. package/test/golden/etag_cache/types.rs +13 -0
  138. 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
+ }