typespec-rust-emitter 0.1.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/README.md +220 -0
- package/dist/src/emitter.d.ts +7 -0
- package/dist/src/emitter.js +490 -0
- package/dist/src/emitter.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lib.d.ts +12 -0
- package/dist/src/lib.js +7 -0
- package/dist/src/lib.js.map +1 -0
- package/dist/src/testing/index.d.ts +2 -0
- package/dist/src/testing/index.js +8 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/test/hello.test.d.ts +1 -0
- package/dist/test/hello.test.js +140 -0
- package/dist/test/hello.test.js.map +1 -0
- package/dist/test/test-host.d.ts +4 -0
- package/dist/test/test-host.js +16 -0
- package/dist/test/test-host.js.map +1 -0
- package/eslint.config.js +20 -0
- package/example/lib/learning/models.tsp +189 -0
- package/example/lib/learning/operations.tsp +319 -0
- package/example/main.tsp +8 -0
- package/example/output-rust/Cargo.lock +1731 -0
- package/example/output-rust/Cargo.toml +12 -0
- package/example/output-rust/src/generated/mod.rs +1 -0
- package/example/output-rust/src/generated/types.rs +315 -0
- package/example/output-rust/src/main.rs +5 -0
- package/example/output-rust/src/mod.rs +1 -0
- package/example/package-lock.json +1495 -0
- package/example/package.json +15 -0
- package/example/tspconfig.yaml +10 -0
- package/justfile +15 -0
- package/package.json +64 -0
- package/prettierrc.yaml +8 -0
- package/src/emitter.ts +685 -0
- package/src/index.ts +3 -0
- package/src/lib.ts +8 -0
- package/src/lib.tsp +6 -0
- package/src/testing/index.ts +8 -0
- package/test/hello.test.ts +168 -0
- package/test/test-host.ts +20 -0
- package/tsconfig.json +18 -0
package/src/emitter.ts
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EmitContext,
|
|
3
|
+
emitFile,
|
|
4
|
+
resolvePath,
|
|
5
|
+
navigateProgram,
|
|
6
|
+
getDoc,
|
|
7
|
+
isArrayModelType,
|
|
8
|
+
isRecordModelType,
|
|
9
|
+
getFormat,
|
|
10
|
+
getPattern,
|
|
11
|
+
isErrorModel,
|
|
12
|
+
Type,
|
|
13
|
+
Model,
|
|
14
|
+
ModelProperty,
|
|
15
|
+
Enum,
|
|
16
|
+
Union,
|
|
17
|
+
Scalar,
|
|
18
|
+
Program,
|
|
19
|
+
Namespace,
|
|
20
|
+
IntrinsicType,
|
|
21
|
+
StringLiteral,
|
|
22
|
+
DecoratorContext,
|
|
23
|
+
getNamespaceFullName,
|
|
24
|
+
} from "@typespec/compiler";
|
|
25
|
+
|
|
26
|
+
export interface RustEmitterOptions {
|
|
27
|
+
moduleName?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface RustDeriveInfo {
|
|
31
|
+
derives: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const rustDeriveKey = Symbol("rustDerive");
|
|
35
|
+
|
|
36
|
+
export function $rustDerive(
|
|
37
|
+
context: DecoratorContext,
|
|
38
|
+
target: Type,
|
|
39
|
+
derive: string,
|
|
40
|
+
) {
|
|
41
|
+
if (target.kind !== "Model") {
|
|
42
|
+
context.program.reportDiagnostic({
|
|
43
|
+
code: "rust-derive-invalid-target",
|
|
44
|
+
message: `@rustDerive can only be applied to models`,
|
|
45
|
+
severity: "error",
|
|
46
|
+
target: context.decoratorTarget,
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const model = target as Model;
|
|
52
|
+
const ns = model.namespace ? getNamespaceFullName(model.namespace) : "";
|
|
53
|
+
|
|
54
|
+
if (!ns.startsWith("TypeSpec")) {
|
|
55
|
+
const info = (model as any)[rustDeriveKey] as RustDeriveInfo | undefined;
|
|
56
|
+
if (info) {
|
|
57
|
+
if (!info.derives.includes(derive)) {
|
|
58
|
+
info.derives.push(derive);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
(model as any)[rustDeriveKey] = { derives: [derive] };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function $rustDerives(
|
|
67
|
+
context: DecoratorContext,
|
|
68
|
+
target: Type,
|
|
69
|
+
...derives: string[]
|
|
70
|
+
) {
|
|
71
|
+
for (const derive of derives) {
|
|
72
|
+
$rustDerive(context, target, derive);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const scalarToRust: Record<string, string> = {
|
|
77
|
+
string: "String",
|
|
78
|
+
int8: "i8",
|
|
79
|
+
int16: "i16",
|
|
80
|
+
int32: "i32",
|
|
81
|
+
int64: "i64",
|
|
82
|
+
uint8: "u8",
|
|
83
|
+
uint16: "u16",
|
|
84
|
+
uint32: "u32",
|
|
85
|
+
uint64: "u64",
|
|
86
|
+
float32: "f32",
|
|
87
|
+
float64: "f64",
|
|
88
|
+
boolean: "bool",
|
|
89
|
+
bytes: "Vec<u8>",
|
|
90
|
+
plainDate: "String",
|
|
91
|
+
plainTime: "String",
|
|
92
|
+
utcDateTime: "String",
|
|
93
|
+
offsetDateTime: "String",
|
|
94
|
+
duration: "String",
|
|
95
|
+
numeric: "f64",
|
|
96
|
+
integer: "i64",
|
|
97
|
+
float: "f64",
|
|
98
|
+
safeint: "i64",
|
|
99
|
+
decimal: "f64",
|
|
100
|
+
decimal128: "f64",
|
|
101
|
+
url: "String",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const formatToRust: Record<string, string> = {
|
|
105
|
+
uuid: "uuid::Uuid",
|
|
106
|
+
date: "chrono::NaiveDate",
|
|
107
|
+
time: "chrono::NaiveTime",
|
|
108
|
+
dateTime: "chrono::DateTime<chrono::Utc>",
|
|
109
|
+
"date-time": "chrono::DateTime<chrono::Utc>",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const typeSpecNamespaces = new Set([
|
|
113
|
+
"TypeSpec",
|
|
114
|
+
"@typespec/http",
|
|
115
|
+
"@typespec/rest",
|
|
116
|
+
"@typespec/openapi",
|
|
117
|
+
"@typespec/openapi3",
|
|
118
|
+
"@typespec/json-schema",
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
function isStdLibNamespace(ns: Namespace | undefined): boolean {
|
|
122
|
+
if (!ns) return false;
|
|
123
|
+
const fullName = ns.name;
|
|
124
|
+
if (typeSpecNamespaces.has(fullName)) return true;
|
|
125
|
+
if (ns.namespace) return isStdLibNamespace(ns.namespace);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isStdLibType(type: Type): boolean {
|
|
130
|
+
if ("namespace" in type) {
|
|
131
|
+
const ns = (type as Model | Enum | Union | Scalar).namespace;
|
|
132
|
+
if (isStdLibNamespace(ns as Namespace)) return true;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface AnonymousStringLiteralUnion {
|
|
138
|
+
enumName: string;
|
|
139
|
+
variants: StringLiteral[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getRustTypeForProperty(
|
|
143
|
+
type: Type,
|
|
144
|
+
program: Program,
|
|
145
|
+
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
146
|
+
): { type: string; isStringLiteral: boolean; stringLiteralValue?: string } {
|
|
147
|
+
const kind = type.kind as string;
|
|
148
|
+
|
|
149
|
+
if (kind === "Model") {
|
|
150
|
+
const model = type as Model;
|
|
151
|
+
if (isArrayModelType(model) && model.indexer?.value) {
|
|
152
|
+
const element = getRustTypeForProperty(
|
|
153
|
+
model.indexer.value,
|
|
154
|
+
program,
|
|
155
|
+
anonymousEnums,
|
|
156
|
+
);
|
|
157
|
+
return { type: `Vec<${element.type}>`, isStringLiteral: false };
|
|
158
|
+
}
|
|
159
|
+
if (isRecordModelType(model) && model.indexer?.value) {
|
|
160
|
+
const value = getRustTypeForProperty(
|
|
161
|
+
model.indexer.value,
|
|
162
|
+
program,
|
|
163
|
+
anonymousEnums,
|
|
164
|
+
);
|
|
165
|
+
return {
|
|
166
|
+
type: `std::collections::HashMap<String, ${value.type}>`,
|
|
167
|
+
isStringLiteral: false,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return { type: toPascalCase(model.name), isStringLiteral: false };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (kind === "ModelProperty") {
|
|
174
|
+
return getRustTypeForProperty(
|
|
175
|
+
(type as ModelProperty).type,
|
|
176
|
+
program,
|
|
177
|
+
anonymousEnums,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (kind === "Enum") {
|
|
182
|
+
return { type: toPascalCase((type as Enum).name), isStringLiteral: false };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (kind === "Union") {
|
|
186
|
+
const unionType = type as Union;
|
|
187
|
+
const variants = Array.from(unionType.variants.values());
|
|
188
|
+
const allStringLiterals = variants.every((v) => v.type.kind === "String");
|
|
189
|
+
|
|
190
|
+
if (allStringLiterals) {
|
|
191
|
+
if (unionType.name) {
|
|
192
|
+
return {
|
|
193
|
+
type: toPascalCase(unionType.name),
|
|
194
|
+
isStringLiteral: false,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const values = variants.map((v) => (v.type as StringLiteral).value);
|
|
198
|
+
const enumName = `Enum_${values.slice(0, 2).join("_")}_${values.length}`;
|
|
199
|
+
if (!anonymousEnums.has(enumName)) {
|
|
200
|
+
anonymousEnums.set(enumName, {
|
|
201
|
+
enumName,
|
|
202
|
+
variants: variants.map((v) => v.type as StringLiteral),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return { type: enumName, isStringLiteral: false };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const nonNullVariants = variants.filter((v) => {
|
|
209
|
+
const vt = v.type;
|
|
210
|
+
if ((vt.kind as string) === "Null") return false;
|
|
211
|
+
if (
|
|
212
|
+
(vt.kind as string) === "Intrinsic" &&
|
|
213
|
+
(vt as IntrinsicType).name === "null"
|
|
214
|
+
)
|
|
215
|
+
return false;
|
|
216
|
+
return true;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
nonNullVariants.length === 1 &&
|
|
221
|
+
(variants.length === 2 ||
|
|
222
|
+
variants.some((v) => {
|
|
223
|
+
const vt = v.type;
|
|
224
|
+
return (
|
|
225
|
+
(vt.kind as string) === "Null" ||
|
|
226
|
+
((vt.kind as string) === "Intrinsic" &&
|
|
227
|
+
(vt as IntrinsicType).name === "null")
|
|
228
|
+
);
|
|
229
|
+
}))
|
|
230
|
+
) {
|
|
231
|
+
const vt = getRustTypeForProperty(
|
|
232
|
+
nonNullVariants[0].type,
|
|
233
|
+
program,
|
|
234
|
+
anonymousEnums,
|
|
235
|
+
);
|
|
236
|
+
return { type: `Option<${vt.type}>`, isStringLiteral: false };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const variantStrings: string[] = [];
|
|
240
|
+
for (const v of variants) {
|
|
241
|
+
const vt = v.type;
|
|
242
|
+
if ((vt.kind as string) === "Null") continue;
|
|
243
|
+
if (
|
|
244
|
+
(vt.kind as string) === "Intrinsic" &&
|
|
245
|
+
(vt as IntrinsicType).name === "null"
|
|
246
|
+
)
|
|
247
|
+
continue;
|
|
248
|
+
const rustVt = getRustTypeForProperty(vt, program, anonymousEnums);
|
|
249
|
+
variantStrings.push(rustVt.type);
|
|
250
|
+
}
|
|
251
|
+
const uniqueTypes = [...new Set(variantStrings)];
|
|
252
|
+
const resultType =
|
|
253
|
+
uniqueTypes.length === 1
|
|
254
|
+
? uniqueTypes[0]
|
|
255
|
+
: `(${uniqueTypes.join(" | ")})`;
|
|
256
|
+
return { type: resultType, isStringLiteral: false };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (kind === "Scalar") {
|
|
260
|
+
const scalar = type as Scalar;
|
|
261
|
+
const format = getFormat(program, scalar);
|
|
262
|
+
const pattern = getPattern(program, scalar);
|
|
263
|
+
|
|
264
|
+
if (format && formatToRust[format] && !pattern) {
|
|
265
|
+
return { type: formatToRust[format], isStringLiteral: false };
|
|
266
|
+
}
|
|
267
|
+
if (pattern) {
|
|
268
|
+
return { type: toPascalCase(scalar.name), isStringLiteral: false };
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
type: scalarToRust[scalar.name] ?? scalar.name,
|
|
272
|
+
isStringLiteral: false,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (kind === "Intrinsic") {
|
|
277
|
+
const intrinsic = type as IntrinsicType;
|
|
278
|
+
const format = getFormat(program, type);
|
|
279
|
+
if (format && formatToRust[format]) {
|
|
280
|
+
return { type: formatToRust[format], isStringLiteral: false };
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
type: scalarToRust[intrinsic.name] ?? "serde_json::Value",
|
|
284
|
+
isStringLiteral: false,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (kind === "String") {
|
|
289
|
+
return { type: "String", isStringLiteral: false };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (kind === "StringLiteral") {
|
|
293
|
+
return {
|
|
294
|
+
type: "String",
|
|
295
|
+
isStringLiteral: true,
|
|
296
|
+
stringLiteralValue: (type as StringLiteral).value,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (kind === "Boolean" || kind === "BooleanLiteral") {
|
|
301
|
+
return { type: "bool", isStringLiteral: false };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (kind === "Number" || kind === "NumericLiteral") {
|
|
305
|
+
return { type: "f64", isStringLiteral: false };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { type: "serde_json::Value", isStringLiteral: false };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function toRustIdent(name: string): string {
|
|
312
|
+
const snakeCase = name.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
|
313
|
+
const result = snakeCase.replace(/[^a-z0-9_]/g, "_");
|
|
314
|
+
if (/^[0-9]/.test(result)) {
|
|
315
|
+
return "_" + result;
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function toPascalCase(name: string): string {
|
|
321
|
+
if (/^[^a-z]+$/.test(name)) {
|
|
322
|
+
return name;
|
|
323
|
+
}
|
|
324
|
+
if (/[-_]/.test(name)) {
|
|
325
|
+
return name
|
|
326
|
+
.split(/[-_]/)
|
|
327
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
328
|
+
.join("");
|
|
329
|
+
}
|
|
330
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function toRustVariantName(name: string): string {
|
|
334
|
+
return toPascalCase(name);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function formatDoc(doc: string | undefined): string {
|
|
338
|
+
if (!doc) return "";
|
|
339
|
+
return doc
|
|
340
|
+
.split("\n")
|
|
341
|
+
.map((line) => `/// ${line.trim()}`)
|
|
342
|
+
.join("\n");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function emitStringLiteralUnion(union: Union): string {
|
|
346
|
+
const parts: string[] = [];
|
|
347
|
+
const name = toPascalCase(union.name ?? "Value");
|
|
348
|
+
const variants = Array.from(union.variants.values());
|
|
349
|
+
|
|
350
|
+
parts.push(
|
|
351
|
+
`#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub enum ${name} {`,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
for (const variant of variants) {
|
|
355
|
+
const literalType = variant.type as StringLiteral;
|
|
356
|
+
const variantName = toRustVariantName(literalType.value);
|
|
357
|
+
const serdeValue = literalType.value;
|
|
358
|
+
parts.push(` #[serde(rename = "${serdeValue}")]`);
|
|
359
|
+
parts.push(` ${variantName},`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
parts.push("}");
|
|
363
|
+
const defaultVariant = toRustVariantName(
|
|
364
|
+
variants[0]?.type ? (variants[0].type as StringLiteral).value : "",
|
|
365
|
+
);
|
|
366
|
+
parts.push(
|
|
367
|
+
`\n\nimpl Default for ${name} {\n fn default() -> Self {\n ${name}::${defaultVariant}\n }\n}`,
|
|
368
|
+
);
|
|
369
|
+
return parts.join("\n");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function emitModel(
|
|
373
|
+
model: Model,
|
|
374
|
+
program: Program,
|
|
375
|
+
anonymousEnums: Map<string, AnonymousStringLiteralUnion>,
|
|
376
|
+
): string {
|
|
377
|
+
const parts: string[] = [];
|
|
378
|
+
const name = toPascalCase(model.name);
|
|
379
|
+
const allProps = getAllProperties(model, program);
|
|
380
|
+
const isError = isErrorModel(program, model);
|
|
381
|
+
|
|
382
|
+
const deriveAttrs = [
|
|
383
|
+
"Debug",
|
|
384
|
+
"Clone",
|
|
385
|
+
"serde::Serialize",
|
|
386
|
+
"serde::Deserialize",
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
const customDerives = (model as any)[rustDeriveKey] as
|
|
390
|
+
| RustDeriveInfo
|
|
391
|
+
| undefined;
|
|
392
|
+
if (customDerives) {
|
|
393
|
+
deriveAttrs.push(...customDerives.derives);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (isError) {
|
|
397
|
+
deriveAttrs.push("thiserror::Error");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
parts.push(`#[derive(${deriveAttrs.join(", ")})]`);
|
|
401
|
+
|
|
402
|
+
if (isError && allProps.size > 0) {
|
|
403
|
+
parts.push('#[error("{code}: {message}")]');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
parts.push(`pub struct ${name}`);
|
|
407
|
+
|
|
408
|
+
if (allProps.size > 0) {
|
|
409
|
+
parts.push(" {");
|
|
410
|
+
for (const [propName, prop] of allProps) {
|
|
411
|
+
const doc = getDoc(program, prop);
|
|
412
|
+
const { type: rustType } = getRustTypeForProperty(
|
|
413
|
+
prop.type,
|
|
414
|
+
program,
|
|
415
|
+
anonymousEnums,
|
|
416
|
+
);
|
|
417
|
+
const optional = prop.optional ? true : false;
|
|
418
|
+
const fieldName = toRustIdent(propName);
|
|
419
|
+
const serdeRename =
|
|
420
|
+
propName !== fieldName ? `#[serde(rename = "${propName}")]` : "";
|
|
421
|
+
|
|
422
|
+
if (doc) {
|
|
423
|
+
parts.push(`\n ${formatDoc(doc)}`);
|
|
424
|
+
}
|
|
425
|
+
if (serdeRename) {
|
|
426
|
+
parts.push(` ${serdeRename}`);
|
|
427
|
+
}
|
|
428
|
+
if (optional) {
|
|
429
|
+
parts.push(` #[serde(skip_serializing_if = "Option::is_none")]`);
|
|
430
|
+
}
|
|
431
|
+
parts.push(
|
|
432
|
+
` pub ${fieldName}: ${optional ? `Option<${rustType}>` : rustType},`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
parts.push("}");
|
|
436
|
+
} else {
|
|
437
|
+
parts.push("(());");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return parts.join("\n");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function getAllProperties(
|
|
444
|
+
model: Model,
|
|
445
|
+
_program: Program,
|
|
446
|
+
): Map<string, ModelProperty> {
|
|
447
|
+
const props = new Map<string, ModelProperty>();
|
|
448
|
+
|
|
449
|
+
if (model.baseModel) {
|
|
450
|
+
const baseProps = getAllProperties(model.baseModel, _program);
|
|
451
|
+
for (const [key, value] of baseProps) {
|
|
452
|
+
props.set(key, value);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
for (const [key, value] of model.properties) {
|
|
457
|
+
props.set(key, value);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return props;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function emitEnum(enumType: Enum): string {
|
|
464
|
+
const parts: string[] = [];
|
|
465
|
+
const name = toPascalCase(enumType.name);
|
|
466
|
+
const members = Array.from(enumType.members.values());
|
|
467
|
+
const isString = members.every(
|
|
468
|
+
(m) => m.value === undefined || typeof m.value === "string",
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
if (isString) {
|
|
472
|
+
parts.push(
|
|
473
|
+
`#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub enum ${name} {`,
|
|
474
|
+
);
|
|
475
|
+
for (const member of members) {
|
|
476
|
+
const variantName = toRustVariantName(member.name);
|
|
477
|
+
const serdeValue = member.value ?? member.name;
|
|
478
|
+
parts.push(` #[serde(rename = "${serdeValue}")]`);
|
|
479
|
+
parts.push(` ${variantName},`);
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
parts.push(
|
|
483
|
+
`#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub enum ${name} {`,
|
|
484
|
+
);
|
|
485
|
+
for (const member of members) {
|
|
486
|
+
const variantName = toRustVariantName(member.name);
|
|
487
|
+
const enumValue = member.value ?? 0;
|
|
488
|
+
parts.push(` ${variantName} = ${enumValue},`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
parts.push("}");
|
|
492
|
+
const defaultVariant = toRustVariantName(members[0]?.name ?? "");
|
|
493
|
+
parts.push(
|
|
494
|
+
`\n\nimpl Default for ${name} {\n fn default() -> Self {\n ${name}::${defaultVariant}\n }\n}`,
|
|
495
|
+
);
|
|
496
|
+
return parts.join("\n");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function emitUnion(union: Union, program: Program): string {
|
|
500
|
+
const parts: string[] = [];
|
|
501
|
+
const name = toPascalCase(union.name ?? "Union");
|
|
502
|
+
|
|
503
|
+
const variants = Array.from(union.variants.values());
|
|
504
|
+
if (variants.length === 0) {
|
|
505
|
+
parts.push(
|
|
506
|
+
`#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub enum ${name} {}\nimpl ${name} {\n pub fn new() -> Self { unreachable!() }\n}`,
|
|
507
|
+
);
|
|
508
|
+
return parts.join("\n");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
parts.push(
|
|
512
|
+
`#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\n#[serde(untagged)]\npub enum ${name} {`,
|
|
513
|
+
);
|
|
514
|
+
for (let i = 0; i < variants.length; i++) {
|
|
515
|
+
const variant = variants[i];
|
|
516
|
+
const variantName = `Variant${i + 1}`;
|
|
517
|
+
const { type: rustType } = getRustTypeForProperty(
|
|
518
|
+
variant.type,
|
|
519
|
+
program,
|
|
520
|
+
new Map(),
|
|
521
|
+
);
|
|
522
|
+
parts.push(` ${variantName}(${rustType}),`);
|
|
523
|
+
}
|
|
524
|
+
parts.push("}");
|
|
525
|
+
|
|
526
|
+
return parts.join("\n");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function emitScalar(
|
|
530
|
+
scalar: Scalar,
|
|
531
|
+
program: Program,
|
|
532
|
+
): { typeDef: string; impls: string[] } {
|
|
533
|
+
const format = getFormat(program, scalar);
|
|
534
|
+
const pattern = getPattern(program, scalar);
|
|
535
|
+
const structName = toPascalCase(scalar.name);
|
|
536
|
+
const impls: string[] = [];
|
|
537
|
+
|
|
538
|
+
if (format && formatToRust[format]) {
|
|
539
|
+
return {
|
|
540
|
+
typeDef: `pub type ${structName} = ${formatToRust[format]};`,
|
|
541
|
+
impls: [],
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (pattern) {
|
|
546
|
+
const rustType = scalarToRust[scalar.name] ?? "String";
|
|
547
|
+
impls.push(
|
|
548
|
+
`\nimpl TryFrom<String> for ${structName} {\n type Error = String;\n\n fn try_from(value: String) -> Result<Self, Self::Error> {\n let re = regex::Regex::new(r"${pattern}").unwrap();\n if re.is_match(&value) { Ok(Self(value)) } else { Err(format!("Invalid value: {}", value)) }\n }\n}`,
|
|
549
|
+
);
|
|
550
|
+
impls.push(
|
|
551
|
+
`\nimpl Default for ${structName} {\n fn default() -> Self {\n Self(String::new())\n }\n}`,
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
typeDef: `#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub struct ${structName}(pub ${rustType});`,
|
|
556
|
+
impls,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const rustType = scalarToRust[scalar.name] ?? "serde_json::Value";
|
|
561
|
+
return {
|
|
562
|
+
typeDef: `pub type ${structName} = ${rustType};`,
|
|
563
|
+
impls: [],
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export async function $onEmit(
|
|
568
|
+
context: EmitContext<RustEmitterOptions>,
|
|
569
|
+
_options?: RustEmitterOptions,
|
|
570
|
+
) {
|
|
571
|
+
const content: string[] = [];
|
|
572
|
+
|
|
573
|
+
const models: Model[] = [];
|
|
574
|
+
const enums: Enum[] = [];
|
|
575
|
+
const unions: Union[] = [];
|
|
576
|
+
const scalars: Scalar[] = [];
|
|
577
|
+
const stringLiteralUnions: Union[] = [];
|
|
578
|
+
const anonymousEnums = new Map<string, AnonymousStringLiteralUnion>();
|
|
579
|
+
|
|
580
|
+
const seenTypes = new Set<string>();
|
|
581
|
+
|
|
582
|
+
navigateProgram(context.program, {
|
|
583
|
+
model(model: Model) {
|
|
584
|
+
if (model.name && !seenTypes.has(model.name) && !isStdLibType(model)) {
|
|
585
|
+
seenTypes.add(model.name);
|
|
586
|
+
models.push(model);
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
enum(enumType: Enum) {
|
|
590
|
+
if (
|
|
591
|
+
enumType.name &&
|
|
592
|
+
!seenTypes.has(enumType.name) &&
|
|
593
|
+
!isStdLibType(enumType)
|
|
594
|
+
) {
|
|
595
|
+
seenTypes.add(enumType.name);
|
|
596
|
+
enums.push(enumType);
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
union(unionType: Union) {
|
|
600
|
+
if (
|
|
601
|
+
unionType.name &&
|
|
602
|
+
!seenTypes.has(unionType.name) &&
|
|
603
|
+
!isStdLibType(unionType)
|
|
604
|
+
) {
|
|
605
|
+
seenTypes.add(unionType.name);
|
|
606
|
+
|
|
607
|
+
const variants = Array.from(unionType.variants.values());
|
|
608
|
+
const allStringLiterals = variants.every(
|
|
609
|
+
(v) => (v.type.kind as string) === "String",
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
if (allStringLiterals) {
|
|
613
|
+
stringLiteralUnions.push(unionType);
|
|
614
|
+
} else {
|
|
615
|
+
unions.push(unionType);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
scalar(scalarType: Scalar) {
|
|
620
|
+
if (
|
|
621
|
+
scalarType.name &&
|
|
622
|
+
!seenTypes.has(scalarType.name) &&
|
|
623
|
+
!isStdLibType(scalarType)
|
|
624
|
+
) {
|
|
625
|
+
seenTypes.add(scalarType.name);
|
|
626
|
+
scalars.push(scalarType);
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
for (const model of models) {
|
|
632
|
+
content.push(emitModel(model, context.program, anonymousEnums));
|
|
633
|
+
content.push("");
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
for (const [enumName, anonEnum] of anonymousEnums) {
|
|
637
|
+
const parts: string[] = [];
|
|
638
|
+
parts.push(
|
|
639
|
+
`#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub enum ${enumName} {`,
|
|
640
|
+
);
|
|
641
|
+
for (const literal of anonEnum.variants) {
|
|
642
|
+
const variantName = toRustVariantName(literal.value);
|
|
643
|
+
parts.push(` #[serde(rename = "${literal.value}")]`);
|
|
644
|
+
parts.push(` ${variantName},`);
|
|
645
|
+
}
|
|
646
|
+
parts.push("}");
|
|
647
|
+
const defaultVariant = toRustVariantName(anonEnum.variants[0]?.value ?? "");
|
|
648
|
+
parts.push(
|
|
649
|
+
`\n\nimpl Default for ${enumName} {\n fn default() -> Self {\n ${enumName}::${defaultVariant}\n }\n}`,
|
|
650
|
+
);
|
|
651
|
+
content.push(parts.join("\n"));
|
|
652
|
+
content.push("");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
for (const stringLiteralUnion of stringLiteralUnions) {
|
|
656
|
+
content.push(emitStringLiteralUnion(stringLiteralUnion));
|
|
657
|
+
content.push("");
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
for (const enumType of enums) {
|
|
661
|
+
content.push(emitEnum(enumType));
|
|
662
|
+
content.push("");
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
for (const unionType of unions) {
|
|
666
|
+
content.push(emitUnion(unionType, context.program));
|
|
667
|
+
content.push("");
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
for (const scalarType of scalars) {
|
|
671
|
+
const { typeDef, impls } = emitScalar(scalarType, context.program);
|
|
672
|
+
content.push(typeDef);
|
|
673
|
+
for (const impl of impls) {
|
|
674
|
+
content.push(impl);
|
|
675
|
+
}
|
|
676
|
+
content.push("");
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const outputContent = content.join("\n");
|
|
680
|
+
|
|
681
|
+
await emitFile(context.program, {
|
|
682
|
+
path: resolvePath(context.emitterOutputDir, "types.rs"),
|
|
683
|
+
content: outputContent,
|
|
684
|
+
});
|
|
685
|
+
}
|
package/src/index.ts
ADDED
package/src/lib.ts
ADDED
package/src/lib.tsp
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { resolvePath } from "@typespec/compiler";
|
|
2
|
+
import { createTestLibrary, TypeSpecTestLibrary } from "@typespec/compiler/testing";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
export const TypespecEmitterTestLibrary: TypeSpecTestLibrary = createTestLibrary({
|
|
6
|
+
name: "typespec-emitter",
|
|
7
|
+
packageRoot: resolvePath(fileURLToPath(import.meta.url), "../../../../"),
|
|
8
|
+
});
|