microsoft/typespec
Publicmirrored from https://github.com/microsoft/typespecAvailable
packages/asset-emitter/test/emitter.test.ts
739lines · modecode
| 1 | import type { |
| 2 | Enum, |
| 3 | Interface, |
| 4 | Model, |
| 5 | ModelProperty, |
| 6 | Operation, |
| 7 | Program, |
| 8 | Scalar, |
| 9 | Type, |
| 10 | Union, |
| 11 | } from "@typespec/compiler"; |
| 12 | import assert from "assert"; |
| 13 | import * as prettier from "prettier"; |
| 14 | import { describe, it } from "vitest"; |
| 15 | import { |
| 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"; |
| 34 | import { emitTypeSpec, getHostForTypeSpecFile } from "./host.js"; |
| 35 | import { TypeScriptInterfaceEmitter } from "./typescript-emitter.js"; |
| 36 | |
| 37 | const testCode = ` |
| 38 | model Basic { x: string } |
| 39 | model RefsOtherModel { x: Basic, y: UnionDecl } |
| 40 | model HasNestedLiteral { x: { y: string } } |
| 41 | model HasArrayProperty { x: string[], y: Basic[] } |
| 42 | model IsArray is Array<string>; |
| 43 | model Derived extends Basic { } |
| 44 | |
| 45 | @doc("Has a doc") |
| 46 | model HasDoc { @doc("an x property") x: string } |
| 47 | |
| 48 | model Template<T> { prop: T } |
| 49 | model HasTemplates { x: Template<Basic> } |
| 50 | model IsTemplate is Template<Basic>; |
| 51 | model HasRef { |
| 52 | x: Basic.x; |
| 53 | y: RefsOtherModel.x; |
| 54 | } |
| 55 | |
| 56 | op SomeOp(x: string): string; |
| 57 | |
| 58 | interface MyInterface { |
| 59 | op get(): string; |
| 60 | } |
| 61 | |
| 62 | union UnionDecl { |
| 63 | x: int32; |
| 64 | y: string; |
| 65 | } |
| 66 | |
| 67 | enum MyEnum { |
| 68 | a: "hi"; |
| 69 | b: "bye"; |
| 70 | } |
| 71 | `; |
| 72 | |
| 73 | class 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 | |
| 91 | async 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 | |
| 98 | describe("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 | |
| 500 | it("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 | |
| 559 | it("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 | |
| 621 | it("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 | |
| 640 | describe("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 | |