microsoft/typespec

Public

mirrored from https://github.com/microsoft/typespecAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/add-xml-model-enum-test

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

packages/asset-emitter/test/emitter.test.ts

739lines · modecode

1import type {
2 Enum,
3 Interface,
4 Model,
5 ModelProperty,
6 Operation,
7 Program,
8 Scalar,
9 Type,
10 Union,
11} from "@typespec/compiler";
12import assert from "assert";
13import * as prettier from "prettier";
14import { describe, it } from "vitest";
15import {
16 ArrayBuilder,
17 type AssetEmitter,
18 CodeTypeEmitter,
19 type Context,
20 Declaration,
21 type EmitEntity,
22 type EmittedSourceFile,
23 type EmitterOutput,
24 ObjectBuilder,
25 Placeholder,
26 type Scope,
27 type SourceFile,
28 StringBuilder,
29 TypeEmitter,
30 type TypeSpecDeclaration,
31 code,
32 createAssetEmitter,
33} from "../src/index.js";
34import { emitTypeSpec, getHostForTypeSpecFile } from "./host.js";
35import { TypeScriptInterfaceEmitter } from "./typescript-emitter.js";
36
37const testCode = `
38model Basic { x: string }
39model RefsOtherModel { x: Basic, y: UnionDecl }
40model HasNestedLiteral { x: { y: string } }
41model HasArrayProperty { x: string[], y: Basic[] }
42model IsArray is Array<string>;
43model Derived extends Basic { }
44
45@doc("Has a doc")
46model HasDoc { @doc("an x property") x: string }
47
48model Template<T> { prop: T }
49model HasTemplates { x: Template<Basic> }
50model IsTemplate is Template<Basic>;
51model HasRef {
52 x: Basic.x;
53 y: RefsOtherModel.x;
54}
55
56op SomeOp(x: string): string;
57
58interface MyInterface {
59 op get(): string;
60}
61
62union UnionDecl {
63 x: int32;
64 y: string;
65}
66
67enum MyEnum {
68 a: "hi";
69 b: "bye";
70}
71`;
72
73class SingleFileEmitter extends TypeScriptInterfaceEmitter {
74 programContext(): Context {
75 const outputFile = this.emitter.createSourceFile("output.ts");
76 return { scope: outputFile.globalScope };
77 }
78
79 operationReturnTypeReferenceContext(operation: Operation, returnType: Type): Context {
80 return {
81 fromOperation: true,
82 };
83 }
84
85 modelDeclaration(model: Model, name: string): EmitterOutput<string> {
86 const newName = this.emitter.getContext().fromOperation ? name + "FromOperation" : name;
87 return super.modelDeclaration(model, newName);
88 }
89}
90
91async function emitTypeSpecToTs(code: string) {
92 const emitter = await emitTypeSpec(SingleFileEmitter, code, {}, false);
93
94 const sf = await emitter.getProgram().host.readFile("./tsp-output/output.ts");
95 return sf.text;
96}
97
98describe("emitter-framework: typescript emitter", () => {
99 it("emits models", async () => {
100 const contents = await emitTypeSpecToTs(`
101 model A {
102 x: {
103 y: string;
104 },
105 }
106 `);
107
108 assert.match(contents, /export interface A/);
109 assert.match(contents, /x: \{ y: string \}/);
110 });
111
112 it("emits model templates", async () => {
113 const contents = await emitTypeSpecToTs(`
114 model Template<T> {
115 x: T
116 }
117
118 model Test1 is Template<string>;
119 model Test2 {
120 prop: Template<int32>;
121 }
122 `);
123
124 assert.match(contents, /interface Test1/);
125 assert.match(contents, /interface TemplateInt32/);
126 assert.match(contents, /interface Test2/);
127 assert.match(contents, /prop: TemplateInt32/);
128 });
129
130 it("emits literal types", async () => {
131 const contents = await emitTypeSpecToTs(`
132 model A {
133 x: true,
134 y: "hi",
135 z: 12
136 }
137 `);
138
139 assert.match(contents, /x: true/);
140 assert.match(contents, /y: "hi"/);
141 assert.match(contents, /z: 12/);
142 });
143
144 it("emits unknown", async () => {
145 const contents = await emitTypeSpecToTs(`
146 model A {
147 x: unknown
148 }
149 `);
150
151 assert.match(contents, /x: unknown/);
152 });
153
154 it("emits array literals", async () => {
155 const contents = await emitTypeSpecToTs(`
156 model MyArray2 is Array<string>;
157
158 model HasArray {
159 x: MyArray2;
160 y: string[];
161 z: (string | int32)[]
162 }
163 `);
164
165 assert.match(contents, /MyArray2 extends Array<string>/);
166 assert.match(contents, /x: MyArray2/);
167 assert.match(contents, /y: string\[\]/);
168 assert.match(contents, /z: \(string \| number\)\[\]/);
169 });
170
171 it("emits arrays of unknown", async () => {
172 const contents = await emitTypeSpecToTs(`
173 model MyArray2 is Array<unknown>;
174 `);
175
176 assert.match(contents, /MyArray2 extends Array<unknown>/);
177 });
178
179 // todo: what to do with optionals not at the end??
180 it("emits operations", async () => {
181 const contents = await emitTypeSpecToTs(`
182 model SomeModel {
183 x: string;
184 }
185 op read(x: string, y: int32, z: { inline: true }, q?: SomeModel): string;
186 `);
187
188 assert.match(contents, /interface read/);
189 assert.match(contents, /x: string/);
190 assert.match(contents, /y: number/);
191 assert.match(contents, /z: { inline: true }/);
192 assert.match(contents, /q?: SomeModel/);
193 });
194
195 it("emits interfaces", async () => {
196 const contents = await emitTypeSpecToTs(`
197 model Foo {
198 prop: string;
199 }
200 op Callback(x: string): string;
201
202 interface Things {
203 op read(x: string): string;
204 op write(y: Foo): Foo;
205 op callCb(cb: Callback): string;
206 }
207
208 interface Template<T> {
209 op read(): T;
210 op write(): T;
211 }
212
213 interface TemplateThings extends Template<string> {}
214 `);
215
216 assert.match(contents, /export interface Things/);
217 assert.match(contents, /read\(x: string\): string/);
218 assert.match(contents, /write\(y: Foo\): Foo/);
219 assert.match(contents, /callCb\(cb: Callback\): string/);
220 assert.match(contents, /export interface TemplateThings/);
221 assert.match(contents, /read\(\): string/);
222 assert.match(contents, /write\(\): string/);
223 });
224
225 it("emits enums", async () => {
226 const contents = await emitTypeSpecToTs(`
227 enum StringEnum {
228 x; y: "hello";
229 }
230
231 enum NumberEnum {
232 x: 1;
233 y: 2;
234 z: 3;
235 }
236 `);
237
238 assert.match(contents, /enum StringEnum/);
239 assert.match(contents, /x = "x"/);
240 assert.match(contents, /y = "hello"/);
241 assert.match(contents, /x = 1/);
242 });
243
244 it("emits unions", async () => {
245 const contents = await emitTypeSpecToTs(`
246 model SomeModel {
247 a: 1 | 2 | SomeModel;
248 b: TU<string>;
249 };
250
251 union U {
252 x: 1,
253 y: "hello",
254 z: SomeModel
255 }
256
257 union TU<T> {
258 x: T;
259 y: null;
260 }
261
262 `);
263
264 assert.match(contents, /a: 1 \| 2 \| SomeModel/);
265 assert.match(contents, /b: TUString/);
266 assert.match(contents, /export type U = 1 \| "hello" \| SomeModel/);
267 assert.match(contents, /export type TUString = string \| null/);
268 });
269
270 it("emits tuple types", async () => {
271 const contents = await emitTypeSpecToTs(`
272 model Foo {
273 x: [string, int32];
274 }
275 `);
276
277 assert.match(contents, /x: \[string, number\]/);
278 });
279
280 it("emits enum member references", async () => {
281 const contents = await emitTypeSpecToTs(`
282 enum MyEnum {
283 a: "hi";
284 b: "bye";
285 }
286
287 model EnumReference {
288 prop: MyEnum.a;
289 prop2: MyEnum.b;
290 }
291 `);
292 assert.match(contents, /prop: MyEnum.a/);
293 assert.match(contents, /prop2: MyEnum.b/);
294 });
295
296 it("emits scalars", async () => {
297 class TestEmitter extends CodeTypeEmitter {
298 scalarDeclaration(scalar: Scalar, name: string): EmitterOutput<string> {
299 return super.scalarDeclaration(scalar, name);
300 }
301 }
302 await emitTypeSpec(
303 TestEmitter,
304 `
305 scalar X extends string;
306 scalar Y extends numeric;
307 `,
308 {
309 scalarDeclaration: 4,
310 },
311 );
312 });
313
314 it("emits models to a single file", async () => {
315 const host = await getHostForTypeSpecFile(testCode);
316 const emitter = createAssetEmitter(host.program, SingleFileEmitter, {
317 emitterOutputDir: host.program.compilerOptions.outputDir!,
318 options: {},
319 } as any);
320
321 emitter.emitProgram();
322 await emitter.writeOutput();
323
324 const files = await host.program.host.readDir("./tsp-output");
325 assert.strictEqual(files.length, 1);
326 const contents = (await host.program.host.readFile("./tsp-output/output.ts")).text;
327 // some light assertions
328 assert.match(contents, /export interface Basic/);
329 assert.match(contents, /export interface HasRef/);
330 });
331
332 it("emits to multiple files", async () => {
333 const host = await getHostForTypeSpecFile(testCode);
334
335 class ClassPerFileEmitter extends TypeScriptInterfaceEmitter {
336 modelDeclarationContext(model: Model): Context {
337 return this.#declarationContext(model);
338 }
339
340 modelInstantiationContext(model: Model): Context {
341 return this.#declarationContext(model);
342 }
343
344 unionDeclarationContext(union: Union): Context {
345 return this.#declarationContext(union);
346 }
347
348 unionInstantiationContext(union: Union): Context {
349 return this.#declarationContext(union);
350 }
351
352 enumDeclarationContext(en: Enum): Context {
353 return this.#declarationContext(en);
354 }
355
356 arrayDeclarationContext(array: Model): Context {
357 return this.#declarationContext(array);
358 }
359
360 interfaceDeclarationContext(iface: Interface): Context {
361 return this.#declarationContext(iface);
362 }
363
364 operationDeclarationContext(operation: Operation): Context {
365 return this.#declarationContext(operation);
366 }
367
368 #declarationContext(decl: TypeSpecDeclaration) {
369 const name = this.emitter.emitDeclarationName(decl);
370 const outputFile = this.emitter.createSourceFile(`${name}.ts`);
371
372 return { scope: outputFile.globalScope };
373 }
374 }
375 const emitter = createAssetEmitter(host.program, ClassPerFileEmitter, {
376 emitterOutputDir: host.program.compilerOptions.outputDir!,
377 options: {},
378 } as any);
379
380 emitter.emitProgram();
381
382 await emitter.writeOutput();
383
384 const files = new Set(await host.program.host.readDir("./tsp-output"));
385 [
386 "Basic.ts",
387 "RefsOtherModel.ts",
388 "HasNestedLiteral.ts",
389 "HasArrayProperty.ts",
390 "IsArray.ts",
391 "Derived.ts",
392 "HasDoc.ts",
393 "HasTemplates.ts",
394 "TemplateBasic.ts",
395 "IsTemplate.ts",
396 "HasRef.ts",
397 "SomeOp.ts",
398 "MyEnum.ts",
399 "UnionDecl.ts",
400 "MyInterface.ts",
401 ].forEach((file) => {
402 assert(files.has(file), `emits ${file}`);
403 });
404 });
405
406 it("emits to namespaces", async () => {
407 const host = await getHostForTypeSpecFile(testCode);
408
409 class NamespacedEmitter extends TypeScriptInterfaceEmitter {
410 private nsByName: Map<string, Scope<string>> = new Map();
411 programContext(program: Program): Context {
412 const outputFile = emitter.createSourceFile("output.ts");
413 return {
414 scope: outputFile.globalScope,
415 };
416 }
417
418 modelDeclarationContext(model: Model): Context {
419 const name = this.emitter.emitDeclarationName(model);
420 if (!name) return {};
421 const nsName = name.slice(0, 1);
422 let nsScope = this.nsByName.get(nsName);
423 if (!nsScope) {
424 nsScope = this.emitter.createScope({}, nsName, this.emitter.getContext().scope);
425 this.nsByName.set(nsName, nsScope);
426 }
427
428 return {
429 scope: nsScope,
430 };
431 }
432
433 async sourceFile(sourceFile: SourceFile<string>): Promise<EmittedSourceFile> {
434 const emittedSourceFile = await super.sourceFile(sourceFile);
435 emittedSourceFile.contents += emitNamespaces(sourceFile.globalScope);
436 emittedSourceFile.contents = await prettier.format(emittedSourceFile.contents, {
437 parser: "typescript",
438 });
439 return emittedSourceFile;
440
441 function emitNamespaces(scope: Scope<string>) {
442 let res = "";
443 for (const childScope of scope.childScopes) {
444 res += emitNamespace(childScope);
445 }
446 return res;
447 }
448 function emitNamespace(scope: Scope<string>) {
449 let ns = `namespace ${scope.name} {\n`;
450 ns += emitNamespaces(scope);
451 for (const decl of scope.declarations) {
452 ns += decl.value + "\n";
453 }
454 ns += `}\n`;
455
456 return ns;
457 }
458 }
459 }
460 const emitter = createAssetEmitter(host.program, NamespacedEmitter, {
461 emitterOutputDir: host.program.compilerOptions.outputDir!,
462 options: {},
463 } as any);
464 emitter.emitProgram();
465 await emitter.writeOutput();
466 const contents = (await host.compilerHost.readFile("tsp-output/output.ts")).text;
467 assert.match(contents, /namespace B/);
468 assert.match(contents, /namespace R/);
469 assert.match(contents, /namespace H/);
470 assert.match(contents, /namespace I/);
471 assert.match(contents, /namespace D/);
472 assert.match(contents, /B\.Basic/);
473 assert.match(contents, /B\.Basic/);
474 });
475
476 it("handles circular references", async () => {
477 const host = await getHostForTypeSpecFile(`
478 model Foo { prop: Baz }
479 model Baz { prop: Foo }
480 `);
481
482 class SingleFileEmitter extends TypeScriptInterfaceEmitter {
483 programContext() {
484 const outputFile = emitter.createSourceFile("output.ts");
485 return { scope: outputFile.globalScope };
486 }
487 }
488 const emitter: AssetEmitter<string> = createAssetEmitter(host.program, SingleFileEmitter, {
489 emitterOutputDir: host.program.compilerOptions.outputDir!,
490 options: {},
491 } as any);
492 emitter.emitProgram();
493 await emitter.writeOutput();
494 const contents = (await host.compilerHost.readFile("tsp-output/output.ts")).text;
495 assert.match(contents, /prop: Foo/);
496 assert.match(contents, /prop: Baz/);
497 });
498});
499
500it("handles circular references", async () => {
501 let sourceFile: SourceFile<string>;
502 class TestEmitter extends CodeTypeEmitter {
503 programContext(program: Program): Context {
504 sourceFile = this.emitter.createSourceFile("hi.txt");
505 return {
506 scope: sourceFile.globalScope,
507 };
508 }
509
510 modelDeclaration(model: Model, name: string): EmitterOutput<string> {
511 const result = this.emitter.emitModelProperties(model);
512 return this.emitter.result.declaration(model.name, code`model references ${result}`);
513 }
514
515 modelProperties(model: Model): EmitterOutput<string> {
516 const builder = new StringBuilder();
517 for (const prop of model.properties.values()) {
518 builder.push(code`${this.emitter.emitModelProperty(prop)}`);
519 }
520 return this.emitter.result.rawCode(builder);
521 }
522
523 modelPropertyLiteral(property: ModelProperty): EmitterOutput<string> {
524 return this.emitter.result.rawCode(code`${this.emitter.emitTypeReference(property.type)}`);
525 }
526
527 sourceFile(sourceFile: SourceFile<string>): EmittedSourceFile {
528 assert.strictEqual(sourceFile.globalScope.declarations.length, 2);
529
530 for (const decl of sourceFile.globalScope.declarations) {
531 if (decl.name === "Foo") {
532 assert.strictEqual(decl.value, "model references Bar");
533 } else {
534 assert.strictEqual(decl.value, "model references Foo");
535 }
536 }
537
538 return {
539 contents: "",
540 path: "",
541 };
542 }
543 }
544
545 await emitTypeSpec(
546 TestEmitter,
547 `
548 model Bar { bProp: Foo };
549 model Foo { fProp: Bar };
550 `,
551 {
552 modelDeclaration: 2,
553 modelProperties: 2,
554 modelPropertyLiteral: 2,
555 },
556 );
557});
558
559it("handles multiple circular references", async () => {
560 let sourceFile: SourceFile<string>;
561 class TestEmitter extends CodeTypeEmitter {
562 programContext(program: Program): Context {
563 sourceFile = this.emitter.createSourceFile("hi.txt");
564 return {
565 scope: sourceFile.globalScope,
566 };
567 }
568
569 modelDeclaration(model: Model, name: string): EmitterOutput<string> {
570 const result = this.emitter.emitModelProperties(model);
571 return this.emitter.result.declaration(model.name, code`model references ${result}`);
572 }
573
574 modelProperties(model: Model): EmitterOutput<string> {
575 const builder = new StringBuilder();
576 for (const prop of model.properties.values()) {
577 builder.push(code`${this.emitter.emitModelProperty(prop)}`);
578 }
579 return this.emitter.result.rawCode(builder);
580 }
581
582 modelPropertyLiteral(property: ModelProperty): EmitterOutput<string> {
583 return this.emitter.result.rawCode(code`${this.emitter.emitTypeReference(property.type)}`);
584 }
585
586 sourceFile(sourceFile: SourceFile<string>): EmittedSourceFile {
587 assert.strictEqual(sourceFile.globalScope.declarations.length, 3);
588
589 for (const decl of sourceFile.globalScope.declarations) {
590 if (decl.name === "Foo") {
591 assert.strictEqual(decl.value, "model references BarBar");
592 } else if (decl.name === "Bar") {
593 assert.strictEqual(decl.value, "model references FooBaz");
594 } else if (decl.name === "Baz") {
595 assert.strictEqual(decl.value, "model references FooBar");
596 }
597 }
598
599 return {
600 contents: "",
601 path: "",
602 };
603 }
604 }
605
606 await emitTypeSpec(
607 TestEmitter,
608 `
609 model Bar { prop: Foo, pro2: Baz };
610 model Foo { prop: Bar, prop2: Bar };
611 model Baz { prop: Foo, prop2: Bar };
612 `,
613 {
614 modelDeclaration: 3,
615 modelProperties: 3,
616 modelPropertyLiteral: 6,
617 },
618 );
619});
620
621it("can get options", async () => {
622 let called = false;
623 class TestEmitter extends CodeTypeEmitter {
624 programContext(program: Program) {
625 called = true;
626 assert.strictEqual(this.emitter.getOptions().doThing, "yes");
627 return {};
628 }
629 }
630
631 const host = await getHostForTypeSpecFile(`model Foo { }`);
632 const assetEmitter = createAssetEmitter(host.program, TestEmitter, {
633 emitterOutputDir: host.program.compilerOptions.outputDir!,
634 options: { doThing: "yes" },
635 } as any);
636 assetEmitter.emitProgram();
637 assert(called, "program context should be called");
638});
639
640describe("emitter-framework: object emitter", () => {
641 class TestEmitter extends TypeEmitter<object> {
642 programContext(program: Program): Context {
643 const sourceFile = this.emitter.createSourceFile("test.json");
644 return {
645 scope: sourceFile.globalScope,
646 };
647 }
648
649 modelDeclaration(model: Model, name: string): EmitterOutput<object> {
650 const om = new ObjectBuilder({
651 kind: "model",
652 name,
653 members: this.emitter.emitModelProperties(model),
654 });
655
656 return this.emitter.result.declaration(name, om);
657 }
658
659 modelLiteral(model: Model): EmitterOutput<object> {
660 const om = new ObjectBuilder({
661 kind: "anonymous model",
662 members: this.emitter.emitModelProperties(model),
663 });
664
665 return om;
666 }
667 modelProperties(model: Model): EmitterOutput<object> {
668 const members = new ArrayBuilder();
669
670 for (const p of model.properties.values()) {
671 members.push(this.emitter.emitModelProperty(p));
672 }
673
674 return members;
675 }
676
677 modelPropertyLiteral(property: ModelProperty): EmitterOutput<object> {
678 const om = new ObjectBuilder({
679 kind: "modelProperty",
680 name: property.name,
681 type: this.emitter.emitTypeReference(property.type),
682 });
683
684 return om;
685 }
686
687 reference(
688 targetDeclaration: Declaration<object>,
689 pathUp: Scope<object>[],
690 pathDown: Scope<object>[],
691 commonScope: Scope<object> | null,
692 ): object | EmitEntity<object> {
693 return { $ref: targetDeclaration.name };
694 }
695
696 sourceFile(sourceFile: SourceFile<object>): EmittedSourceFile {
697 const emittedSourceFile: EmittedSourceFile = {
698 path: sourceFile.path,
699 contents: "",
700 };
701
702 const obj: { declarations: object[] } = { declarations: [] };
703 for (const decl of sourceFile.globalScope.declarations) {
704 if (decl.value instanceof Placeholder) {
705 obj.declarations.push({ placeholder: true });
706 } else {
707 obj.declarations.push(decl.value);
708 }
709 }
710
711 emittedSourceFile.contents = JSON.stringify(obj, null, 4);
712 return emittedSourceFile;
713 }
714 }
715
716 it("emits objects", async () => {
717 const host = await getHostForTypeSpecFile(
718 `
719 model Foo {
720 bar: Bar
721 }
722 model Bar {
723 x: Foo;
724 y: {
725 x: Foo
726 };
727 };
728 `,
729 );
730 const assetEmitter = createAssetEmitter(host.program, TestEmitter, {
731 emitterOutputDir: host.program.compilerOptions.outputDir!,
732 options: {},
733 } as any);
734 assetEmitter.emitProgram();
735 await assetEmitter.writeOutput();
736 const contents = JSON.parse((await host.compilerHost.readFile("tsp-output/test.json")!).text);
737 assert.strictEqual(contents.declarations.length, 2);
738 });
739});
740