From 485eb699e6b227da3a1a2395180e028fcf7ef0b8 Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Thu, 15 May 2025 18:03:08 +0000 Subject: [PATCH 01/14] refactor to new pattern, prep for changes --- .../components/union-expression.tsx | 2 +- .../components/union-declaration.test.tsx | 198 ++++++++---------- 2 files changed, 94 insertions(+), 106 deletions(-) diff --git a/packages/emitter-framework/src/typescript/components/union-expression.tsx b/packages/emitter-framework/src/typescript/components/union-expression.tsx index 8fd34fa294..8f027cf850 100644 --- a/packages/emitter-framework/src/typescript/components/union-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/union-expression.tsx @@ -32,7 +32,7 @@ export function UnionExpression({ type, children }: UnionExpressionProps) { if (children || (Array.isArray(children) && children.length)) { return ( <> - {variants} {` | ${children}`} + {variants} {`| ${children}`} ); } diff --git a/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx b/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx index cbc07c6098..da1f233b23 100644 --- a/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx +++ b/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx @@ -1,182 +1,170 @@ import { render } from "@alloy-js/core"; +import { d } from "@alloy-js/core/testing"; import { SourceFile } from "@alloy-js/typescript"; -import { Namespace } from "@typespec/compiler"; -import { format } from "prettier"; -import { assert, describe, expect, it } from "vitest"; +import { Enum, Union } from "@typespec/compiler"; +import { BasicTestRunner } from "@typespec/compiler/testing"; +import { beforeEach, describe, it } from "vitest"; import { Output } from "../../../src/core/components/output.jsx"; import { UnionDeclaration } from "../../../src/typescript/components/union-declaration.js"; import { UnionExpression } from "../../../src/typescript/components/union-expression.js"; -import { getProgram } from "../test-host.js"; +import { assertFileContents } from "../../utils.js"; +import { createEmitterFrameworkTestRunner } from "../test-host.js"; describe("Typescript Union Declaration", () => { + let runner: BasicTestRunner; + + beforeEach(async () => { + runner = await createEmitterFrameworkTestRunner(); + }); + describe("Union not bound to Typespec Types", () => { + // TODO: clean up this test it("creates a union declaration", async () => { - const program = await getProgram(""); + await runner.compile(``); const res = render( - + "red" | "blue" , ); - const testFile = res.contents.find((file) => file.path === "test.ts"); - assert(testFile, "test.ts file not rendered"); - const actualContent = await format(testFile.contents as string, { parser: "typescript" }); - const expectedContent = await format(`type MyUnion = "red" | "blue"`, { - parser: "typescript", - }); - expect(actualContent).toBe(expectedContent); + assertFileContents( + res, + d` + type MyUnion = "red" | "blue"; + `, + ); }); }); describe("Union bound to Typespec Types", () => { describe("Bound to Union", () => { it("creates a union declaration", async () => { - const program = await getProgram(` - namespace DemoService; - union TestUnion { - one: "one", - two: "two" - } - `); - - const [namespace] = program.resolveTypeReference("DemoService"); - const union = Array.from((namespace as Namespace).unions.values())[0]; + const { TestUnion } = (await runner.compile(` + namespace DemoService; + @test union TestUnion { + one: "one", + two: "two" + } + `)) as { TestUnion: Union }; const res = render( - + - + , ); - const testFile = res.contents.find((file) => file.path === "test.ts"); - assert(testFile, "test.ts file not rendered"); - const actualContent = await format(testFile.contents as string, { parser: "typescript" }); - const expectedContent = await format(`type TestUnion = "one" | "two"`, { - parser: "typescript", - }); - expect(actualContent).toBe(expectedContent); + assertFileContents( + res, + d` + type TestUnion = "one" | "two"; + `, + ); }); it("creates a union declaration with name override", async () => { - const program = await getProgram(` - namespace DemoService; - union TestUnion { - one: "one", - two: "two" - } - `); - - const [namespace] = program.resolveTypeReference("DemoService"); - const union = Array.from((namespace as Namespace).unions.values())[0]; + const { TestUnion } = (await runner.compile(` + namespace DemoService; + @test union TestUnion { + one: "one", + two: "two" + } + `)) as { TestUnion: Union }; const res = render( - + - + , ); - const testFile = res.contents.find((file) => file.path === "test.ts"); - assert(testFile, "test.ts file not rendered"); - const actualContent = await format(testFile.contents as string, { parser: "typescript" }); - const expectedContent = await format(`export type MyUnion = "one" | "two"`, { - parser: "typescript", - }); - expect(actualContent).toBe(expectedContent); + assertFileContents( + res, + d` + export type MyUnion = "one" | "two"; + `, + ); }); it("creates a union declaration with extra children", async () => { - const program = await getProgram(` - namespace DemoService; - union TestUnion { - one: "one", - two: "two" - } - `); - - const [namespace] = program.resolveTypeReference("DemoService"); - const union = Array.from((namespace as Namespace).unions.values())[0]; + const { TestUnion } = (await runner.compile(` + namespace DemoService; + @test union TestUnion { + one: "one", + two: "two" + } + `)) as { TestUnion: Union }; const res = render( - + - "three" + "three" , ); - const testFile = res.contents.find((file) => file.path === "test.ts"); - assert(testFile, "test.ts file not rendered"); - const actualContent = await format(testFile.contents as string, { parser: "typescript" }); - const expectedContent = await format(`type TestUnion = "one" | "two" | "three"`, { - parser: "typescript", - }); - expect(actualContent).toBe(expectedContent); + assertFileContents( + res, + d` + type TestUnion = "one" | "two" | "three"; + `, + ); }); - it("renders an union expression", async () => { - const program = await getProgram(` + it("renders a union expression", async () => { + const { TestUnion } = (await runner.compile(` namespace DemoService; - union TestUnion { + @test union TestUnion { one: "one", two: "two" } - `); - - const [namespace] = program.resolveTypeReference("DemoService"); - const union = Array.from((namespace as Namespace).unions.values())[0]; + `)) as { TestUnion: Union }; const res = render( - + - let x: = "one"; + let x: = "one"; , ); - const testFile = res.contents.find((file) => file.path === "test.ts"); - assert(testFile, "test.ts file not rendered"); - const actualContent = await format(testFile.contents as string, { parser: "typescript" }); - const expectedContent = await format(`let x:"one" | "two" = "one"`, { - parser: "typescript", - }); - expect(actualContent).toBe(expectedContent); + assertFileContents( + res, + d` + let x: "one" | "two" = "one"; + `, + ); }); }); describe("Bound to Enum", () => { it("creates a union declaration", async () => { - const program = await getProgram(` - namespace DemoService; - enum TestEnum { - one: "one", - two: "two" - } - `); - - const [namespace] = program.resolveTypeReference("DemoService"); - const union = Array.from((namespace as Namespace).enums.values())[0]; + const { TestEnum } = (await runner.compile(` + namespace DemoService; + @test enum TestEnum { + one: "one", + two: "two" + } + `)) as { TestEnum: Enum }; const res = render( - + - + , ); - const testFile = res.contents.find((file) => file.path === "test.ts"); - assert(testFile, "test.ts file not rendered"); - const actualContent = await format(testFile.contents as string, { parser: "typescript" }); - const expectedContent = await format(`type TestEnum = "one" | "two"`, { - parser: "typescript", - }); - expect(actualContent).toBe(expectedContent); + assertFileContents( + res, + d` + type TestEnum = "one" | "two"; + `, + ); }); }); }); From 302aaaada21c5a2c6a2e5d6dde32f541d2fd2335 Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Thu, 15 May 2025 19:45:07 +0000 Subject: [PATCH 02/14] wip --- .../components/interface-declaration.tsx | 2 +- .../components/union-expression.tsx | 24 +++++ .../components/union-declaration.test.tsx | 100 ++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/packages/emitter-framework/src/typescript/components/interface-declaration.tsx b/packages/emitter-framework/src/typescript/components/interface-declaration.tsx index 64a5000e72..889e489998 100644 --- a/packages/emitter-framework/src/typescript/components/interface-declaration.tsx +++ b/packages/emitter-framework/src/typescript/components/interface-declaration.tsx @@ -154,7 +154,7 @@ function InterfaceBody(props: TypedInterfaceDeclarationProps): Children { return ( <> - + {(typeMember) => { return ; }} diff --git a/packages/emitter-framework/src/typescript/components/union-expression.tsx b/packages/emitter-framework/src/typescript/components/union-expression.tsx index 8f027cf850..d33668e6cc 100644 --- a/packages/emitter-framework/src/typescript/components/union-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/union-expression.tsx @@ -17,11 +17,35 @@ export function UnionExpression({ type, children }: UnionExpressionProps) { UnionVariant | EnumMember >; + let discriminatedUnion = undefined; + if ($.union.is(type)) { + discriminatedUnion = $.union.getDiscriminatedUnion(type); + } + const variants = ( {(_, value) => { if ($.enumMember.is(value)) { return ; + } + + if (discriminatedUnion?.options.envelope) { + const discriminatorPropertyName = discriminatedUnion.options.discriminatorPropertyName; + const envelopePropertyName = discriminatedUnion.options.envelopePropertyName; + const envelope = $.model.create({ + properties: { + [discriminatorPropertyName]: $.modelProperty.create({ + name: discriminatedUnion.options.discriminatorPropertyName, + type: $.literal.createString(value.name as string), + }), + [envelopePropertyName]: $.modelProperty.create({ + name: discriminatedUnion.options.envelopePropertyName, + type: value.type, + }), + }, + }); + + return ; } else { return ; } diff --git a/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx b/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx index da1f233b23..0ab1411f30 100644 --- a/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx +++ b/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx @@ -139,6 +139,106 @@ describe("Typescript Union Declaration", () => { `, ); }); + + describe("Discriminated Union", () => { + it("renders a discriminated union declaration", async () => { + const { TestUnion } = (await runner.compile(` + namespace DemoService; + @discriminated + @test union TestUnion { + one: { oneItem: true }, + two: true + } + `)) as { TestUnion: Union }; + + const res = render( + + + + + , + ); + + assertFileContents( + res, + d` + type TestUnion = { + kind: "one"; + value: { + oneItem: true; + }; + } | { + kind: "two"; + value: true; + }; + `, + ); + }); + }); + + it("renders a discriminated union declaration with custom properties", async () => { + const { TestUnion } = (await runner.compile(` + namespace DemoService; + @discriminated(#{ discriminatorPropertyName: "dataKind", envelopePropertyName: "data" }) + @test union TestUnion { + one: { oneItem: true }, + two: true + } + `)) as { TestUnion: Union }; + + const res = render( + + + + + , + ); + assertFileContents( + res, + d` + type TestUnion = { + dataKind: "one"; + data: { + oneItem: true; + }; + } | { + dataKind: "two"; + data: true; + }; + `, + ); + }); + + it("renders a discriminated union declaration with no envelope", async () => { + const { TestUnion } = (await runner.compile(` + namespace DemoService; + @discriminated(#{ envelope: "none" }) + @test union TestUnion { + one: { oneItem: true }, + two: { secondItem: false } + } + `)) as { TestUnion: Union }; + + const res = render( + + + + + , + ); + assertFileContents( + res, + d` + type TestUnion = { + kind: "one"; + oneItem: true; + } | { + kind: "two"; + secondItem: false; + }; + `, + ); + }); }); describe("Bound to Enum", () => { From e1b1f6bbac18a89202e67a4d69343dc1ff554f27 Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Thu, 15 May 2025 20:16:55 +0000 Subject: [PATCH 03/14] wip --- .../src/typescript/components/union-expression.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/emitter-framework/src/typescript/components/union-expression.tsx b/packages/emitter-framework/src/typescript/components/union-expression.tsx index d33668e6cc..1f7ecfea46 100644 --- a/packages/emitter-framework/src/typescript/components/union-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/union-expression.tsx @@ -29,9 +29,10 @@ export function UnionExpression({ type, children }: UnionExpressionProps) { return ; } - if (discriminatedUnion?.options.envelope) { + if (discriminatedUnion?.options.envelope === "object") { const discriminatorPropertyName = discriminatedUnion.options.discriminatorPropertyName; const envelopePropertyName = discriminatedUnion.options.envelopePropertyName; + const envelope = $.model.create({ properties: { [discriminatorPropertyName]: $.modelProperty.create({ @@ -46,6 +47,10 @@ export function UnionExpression({ type, children }: UnionExpressionProps) { }); return ; + } else if (discriminatedUnion?.options.envelope === "none") { + // this is a discriminated union with no envelope + // we need a model where the discriminator and the rest of the values are side-by-side + return ; } else { return ; } From 072868a5068714829671601f0dd108740939a5ed Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Thu, 15 May 2025 20:59:11 +0000 Subject: [PATCH 04/14] Refactor --- .../components/union-expression.tsx | 131 +++++++++++++----- 1 file changed, 94 insertions(+), 37 deletions(-) diff --git a/packages/emitter-framework/src/typescript/components/union-expression.tsx b/packages/emitter-framework/src/typescript/components/union-expression.tsx index 1f7ecfea46..8b346daf8c 100644 --- a/packages/emitter-framework/src/typescript/components/union-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/union-expression.tsx @@ -1,7 +1,8 @@ import * as ay from "@alloy-js/core"; import { Children } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; -import { Enum, EnumMember, Union, UnionVariant } from "@typespec/compiler"; +import { compilerAssert, Enum, EnumMember, Union, UnionVariant } from "@typespec/compiler"; +import { Typekit } from "@typespec/compiler/typekit"; import { useTsp } from "../../core/context/tsp-context.js"; import { TypeExpression } from "./type-expression.jsx"; @@ -17,47 +18,13 @@ export function UnionExpression({ type, children }: UnionExpressionProps) { UnionVariant | EnumMember >; - let discriminatedUnion = undefined; - if ($.union.is(type)) { - discriminatedUnion = $.union.getDiscriminatedUnion(type); - } - const variants = ( - {(_, value) => { - if ($.enumMember.is(value)) { - return ; - } - - if (discriminatedUnion?.options.envelope === "object") { - const discriminatorPropertyName = discriminatedUnion.options.discriminatorPropertyName; - const envelopePropertyName = discriminatedUnion.options.envelopePropertyName; - - const envelope = $.model.create({ - properties: { - [discriminatorPropertyName]: $.modelProperty.create({ - name: discriminatedUnion.options.discriminatorPropertyName, - type: $.literal.createString(value.name as string), - }), - [envelopePropertyName]: $.modelProperty.create({ - name: discriminatedUnion.options.envelopePropertyName, - type: value.type, - }), - }, - }); - - return ; - } else if (discriminatedUnion?.options.envelope === "none") { - // this is a discriminated union with no envelope - // we need a model where the discriminator and the rest of the values are side-by-side - return ; - } else { - return ; - } - }} + {(_, value) => renderVariant($, value)} ); + // Handle additional children if present if (children || (Array.isArray(children) && children.length)) { return ( <> @@ -68,3 +35,93 @@ export function UnionExpression({ type, children }: UnionExpressionProps) { return variants; } + +/** + * Renders a union variant or enum member based on its type + */ +function renderVariant($: Typekit, value: UnionVariant | EnumMember) { + if ($.enumMember.is(value)) { + return ; + } + + const discriminatedUnion = $.union.getDiscriminatedUnion(value.union); + switch (discriminatedUnion?.options.envelope) { + case "object": + return ( + + ); + case "none": + return ( + + ); + default: + // not a discriminated union + return ; + } +} + +interface ObjectEnvelopeProps { + type: UnionVariant; + discriminatorPropertyName: string; + envelopePropertyName: string; +} + +/** + * Renders a discriminated union with "object" envelope style + * where model properties are nested inside an envelope + */ +function ObjectEnvelope(props: ObjectEnvelopeProps) { + const { $ } = useTsp(); + + const envelope = $.model.create({ + properties: { + [props.discriminatorPropertyName]: $.modelProperty.create({ + name: props.discriminatorPropertyName, + type: $.literal.createString(props.type.name as string), + }), + [props.envelopePropertyName]: $.modelProperty.create({ + name: props.envelopePropertyName, + type: props.type.type, + }), + }, + }); + + return ; +} + +interface NoneEnvelopeProps { + type: UnionVariant; + discriminatorPropertyName: string; +} + +/** + * Renders a discriminated union with "none" envelope style + * where discriminator property sits alongside model properties + */ +function NoneEnvelope(props: NoneEnvelopeProps) { + const { $ } = useTsp(); + + compilerAssert( + $.model.is(props.type.type), + "Expected all union variants to be models when using a discriminated union with no envelope", + ); + + const model = $.model.create({ + properties: { + [props.discriminatorPropertyName]: $.modelProperty.create({ + name: props.discriminatorPropertyName, + type: $.literal.createString(props.type.name as string), + }), + ...Object.fromEntries(props.type.type.properties), + }, + }); + + return ; +} From b1f10b0f838d7d96a6015de2d8dae68ce54215a1 Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Thu, 15 May 2025 22:11:39 +0000 Subject: [PATCH 05/14] chronus --- .../changes/discriminated-union-ef-2025-4-15-22-11-29.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/discriminated-union-ef-2025-4-15-22-11-29.md diff --git a/.chronus/changes/discriminated-union-ef-2025-4-15-22-11-29.md b/.chronus/changes/discriminated-union-ef-2025-4-15-22-11-29.md new file mode 100644 index 0000000000..c2290f5e33 --- /dev/null +++ b/.chronus/changes/discriminated-union-ef-2025-4-15-22-11-29.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/emitter-framework" +--- + +Render discriminated unions correctly \ No newline at end of file From df8266ce7b17a158284abcd69ded065c2043d69b Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Thu, 15 May 2025 22:19:08 +0000 Subject: [PATCH 06/14] inline --- .../components/union-expression.tsx | 60 +++++++++---------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/packages/emitter-framework/src/typescript/components/union-expression.tsx b/packages/emitter-framework/src/typescript/components/union-expression.tsx index 8b346daf8c..982c09c1c8 100644 --- a/packages/emitter-framework/src/typescript/components/union-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/union-expression.tsx @@ -2,7 +2,6 @@ import * as ay from "@alloy-js/core"; import { Children } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import { compilerAssert, Enum, EnumMember, Union, UnionVariant } from "@typespec/compiler"; -import { Typekit } from "@typespec/compiler/typekit"; import { useTsp } from "../../core/context/tsp-context.js"; import { TypeExpression } from "./type-expression.jsx"; @@ -20,7 +19,33 @@ export function UnionExpression({ type, children }: UnionExpressionProps) { const variants = ( - {(_, value) => renderVariant($, value)} + {(_, type) => { + if ($.enumMember.is(type)) { + return ; + } + + const discriminatedUnion = $.union.getDiscriminatedUnion(type.union); + switch (discriminatedUnion?.options.envelope) { + case "object": + return ( + + ); + case "none": + return ( + + ); + default: + // not a discriminated union + return ; + } + }} ); @@ -36,37 +61,6 @@ export function UnionExpression({ type, children }: UnionExpressionProps) { return variants; } -/** - * Renders a union variant or enum member based on its type - */ -function renderVariant($: Typekit, value: UnionVariant | EnumMember) { - if ($.enumMember.is(value)) { - return ; - } - - const discriminatedUnion = $.union.getDiscriminatedUnion(value.union); - switch (discriminatedUnion?.options.envelope) { - case "object": - return ( - - ); - case "none": - return ( - - ); - default: - // not a discriminated union - return ; - } -} - interface ObjectEnvelopeProps { type: UnionVariant; discriminatorPropertyName: string; From db1a7f46d407e04a60addd5294986197f8a52d0f Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Thu, 15 May 2025 22:49:44 +0000 Subject: [PATCH 07/14] remove comment --- .../test/typescript/components/union-declaration.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx b/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx index 0ab1411f30..30ee783cd6 100644 --- a/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx +++ b/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx @@ -18,7 +18,6 @@ describe("Typescript Union Declaration", () => { }); describe("Union not bound to Typespec Types", () => { - // TODO: clean up this test it("creates a union declaration", async () => { await runner.compile(``); const res = render( From a54dfda93b3ca5cd42efc3a049b06345c8f59ba7 Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Mon, 19 May 2025 23:14:12 +0000 Subject: [PATCH 08/14] use intersection type for named models --- .../components/union-expression.tsx | 24 +- .../components/union-declaration.test.tsx | 205 +++++++++++++----- 2 files changed, 169 insertions(+), 60 deletions(-) diff --git a/packages/emitter-framework/src/typescript/components/union-expression.tsx b/packages/emitter-framework/src/typescript/components/union-expression.tsx index 982c09c1c8..62f35917ee 100644 --- a/packages/emitter-framework/src/typescript/components/union-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/union-expression.tsx @@ -3,6 +3,7 @@ import { Children } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import { compilerAssert, Enum, EnumMember, Union, UnionVariant } from "@typespec/compiler"; import { useTsp } from "../../core/context/tsp-context.js"; +import { efRefkey } from "../utils/refkey.js"; import { TypeExpression } from "./type-expression.jsx"; export interface UnionExpressionProps { @@ -107,15 +108,18 @@ function NoneEnvelope(props: NoneEnvelopeProps) { "Expected all union variants to be models when using a discriminated union with no envelope", ); - const model = $.model.create({ - properties: { - [props.discriminatorPropertyName]: $.modelProperty.create({ - name: props.discriminatorPropertyName, - type: $.literal.createString(props.type.name as string), - }), - ...Object.fromEntries(props.type.type.properties), - }, - }); + if ($.model.isExpresion(props.type.type)) { + const model = $.model.create({ + properties: { + [props.discriminatorPropertyName]: $.modelProperty.create({ + name: props.discriminatorPropertyName, + type: $.literal.createString(props.type.name as string), + }), + ...Object.fromEntries(props.type.type.properties), + }, + }); + return ; + } - return ; + return ay.code`{kind: "${String(props.type.name)}"} & ${efRefkey(props.type.type)}`; } diff --git a/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx b/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx index 30ee783cd6..7c6e885c94 100644 --- a/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx +++ b/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx @@ -1,12 +1,13 @@ import { render } from "@alloy-js/core"; import { d } from "@alloy-js/core/testing"; import { SourceFile } from "@alloy-js/typescript"; -import { Enum, Union } from "@typespec/compiler"; +import { Enum, Model, Union } from "@typespec/compiler"; import { BasicTestRunner } from "@typespec/compiler/testing"; import { beforeEach, describe, it } from "vitest"; import { Output } from "../../../src/core/components/output.jsx"; import { UnionDeclaration } from "../../../src/typescript/components/union-declaration.js"; import { UnionExpression } from "../../../src/typescript/components/union-expression.js"; +import { InterfaceDeclaration } from "../../../src/typescript/index.js"; import { assertFileContents } from "../../utils.js"; import { createEmitterFrameworkTestRunner } from "../test-host.js"; @@ -142,13 +143,13 @@ describe("Typescript Union Declaration", () => { describe("Discriminated Union", () => { it("renders a discriminated union declaration", async () => { const { TestUnion } = (await runner.compile(` - namespace DemoService; - @discriminated - @test union TestUnion { - one: { oneItem: true }, - two: true - } - `)) as { TestUnion: Union }; + namespace DemoService; + @discriminated + @test union TestUnion { + one: { oneItem: true }, + two: true + } + `)) as { TestUnion: Union }; const res = render( @@ -161,15 +162,15 @@ describe("Typescript Union Declaration", () => { assertFileContents( res, d` - type TestUnion = { - kind: "one"; - value: { - oneItem: true; + type TestUnion = { + kind: "one"; + value: { + oneItem: true; + }; + } | { + kind: "two"; + value: true; }; - } | { - kind: "two"; - value: true; - }; `, ); }); @@ -177,13 +178,13 @@ describe("Typescript Union Declaration", () => { it("renders a discriminated union declaration with custom properties", async () => { const { TestUnion } = (await runner.compile(` - namespace DemoService; - @discriminated(#{ discriminatorPropertyName: "dataKind", envelopePropertyName: "data" }) - @test union TestUnion { - one: { oneItem: true }, - two: true - } - `)) as { TestUnion: Union }; + namespace DemoService; + @discriminated(#{ discriminatorPropertyName: "dataKind", envelopePropertyName: "data" }) + @test union TestUnion { + one: { oneItem: true }, + two: true + } + `)) as { TestUnion: Union }; const res = render( @@ -195,49 +196,153 @@ describe("Typescript Union Declaration", () => { assertFileContents( res, d` - type TestUnion = { - dataKind: "one"; - data: { - oneItem: true; + type TestUnion = { + dataKind: "one"; + data: { + oneItem: true; + }; + } | { + dataKind: "two"; + data: true; }; - } | { - dataKind: "two"; - data: true; - }; - `, + `, ); }); - it("renders a discriminated union declaration with no envelope", async () => { - const { TestUnion } = (await runner.compile(` - namespace DemoService; - @discriminated(#{ envelope: "none" }) - @test union TestUnion { - one: { oneItem: true }, - two: { secondItem: false } - } - `)) as { TestUnion: Union }; + it("renders a discriminated union with named models", async () => { + const { Pet, Cat, Dog } = (await runner.compile(` + namespace DemoService; + @test model Cat { + name: string; + meow: boolean; + } + + @test model Dog { + name: string; + bark: boolean; + } + + @discriminated + @test union Pet { + cat: Cat, + dog: Dog, + } + `)) as { Pet: Union; Cat: Model; Dog: Model }; const res = render( - + + + + + , ); assertFileContents( res, d` - type TestUnion = { - kind: "one"; - oneItem: true; - } | { - kind: "two"; - secondItem: false; - }; - `, + interface Cat { + name: string; + meow: boolean; + } + interface Dog { + name: string; + bark: boolean; + } + type Pet = { + kind: "cat"; + value: Cat; + } | { + kind: "dog"; + value: Dog; + }; + `, ); }); + + describe("Discriminated Union with no envelope", () => { + it("renders named discriminated union declarations", async () => { + const { Pet, Cat, Dog } = (await runner.compile(` + namespace DemoService; + + @test model Cat { + name: string; + meow: boolean; + } + + @test model Dog { + name: string; + bark: boolean; + } + + @discriminated(#{ envelope: "none" }) + @test union Pet { + cat: Cat, + dog: Dog, + } + `)) as { Pet: Union; Cat: Model; Dog: Model }; + + const res = render( + + + + + + + + + , + ); + assertFileContents( + res, + d` + interface Cat { + name: string; + meow: boolean; + } + interface Dog { + name: string; + bark: boolean; + } + type Pet = {kind: "cat"} & Cat | {kind: "dog"} & Dog; + `, + ); + }); + + it("renders a discriminated union declaration with no envelope", async () => { + const { TestUnion } = (await runner.compile(` + namespace DemoService; + @discriminated(#{ envelope: "none" }) + @test union TestUnion { + one: { oneItem: true }, + two: { secondItem: false } + } + `)) as { TestUnion: Union }; + + const res = render( + + + + + , + ); + + assertFileContents( + res, + d` + type TestUnion = { + kind: "one"; + oneItem: true; + } | { + kind: "two"; + secondItem: false; + }; + `, + ); + }); + }); }); describe("Bound to Enum", () => { From 4e976ec2b1403362bd068d3db87c1b18bdb97e8b Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Mon, 19 May 2025 23:17:41 +0000 Subject: [PATCH 09/14] use prop name --- .../src/typescript/components/union-expression.tsx | 4 +--- .../test/typescript/components/union-declaration.test.tsx | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/emitter-framework/src/typescript/components/union-expression.tsx b/packages/emitter-framework/src/typescript/components/union-expression.tsx index 62f35917ee..865fbe11db 100644 --- a/packages/emitter-framework/src/typescript/components/union-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/union-expression.tsx @@ -43,14 +43,12 @@ export function UnionExpression({ type, children }: UnionExpressionProps) { /> ); default: - // not a discriminated union return ; } }} ); - // Handle additional children if present if (children || (Array.isArray(children) && children.length)) { return ( <> @@ -121,5 +119,5 @@ function NoneEnvelope(props: NoneEnvelopeProps) { return ; } - return ay.code`{kind: "${String(props.type.name)}"} & ${efRefkey(props.type.type)}`; + return ay.code`{${props.discriminatorPropertyName}: "${String(props.type.name)}"} & ${efRefkey(props.type.type)}`; } diff --git a/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx b/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx index 7c6e885c94..837623a7c4 100644 --- a/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx +++ b/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx @@ -277,7 +277,7 @@ describe("Typescript Union Declaration", () => { bark: boolean; } - @discriminated(#{ envelope: "none" }) + @discriminated(#{ envelope: "none", discriminatorPropertyName: "dataKind" }) @test union Pet { cat: Cat, dog: Dog, @@ -306,7 +306,7 @@ describe("Typescript Union Declaration", () => { name: string; bark: boolean; } - type Pet = {kind: "cat"} & Cat | {kind: "dog"} & Dog; + type Pet = {dataKind: "cat"} & Cat | {dataKind: "dog"} & Dog; `, ); }); From 8946b2cb9ebca9abe78817ee3e2a4c37c5368290 Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Mon, 19 May 2025 23:19:31 +0000 Subject: [PATCH 10/14] comment --- .../src/typescript/components/union-expression.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/emitter-framework/src/typescript/components/union-expression.tsx b/packages/emitter-framework/src/typescript/components/union-expression.tsx index 865fbe11db..63e6255b1d 100644 --- a/packages/emitter-framework/src/typescript/components/union-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/union-expression.tsx @@ -106,6 +106,7 @@ function NoneEnvelope(props: NoneEnvelopeProps) { "Expected all union variants to be models when using a discriminated union with no envelope", ); + // This is an anonymous type, so we render its properties along the discriminator property if ($.model.isExpresion(props.type.type)) { const model = $.model.create({ properties: { From 12347944b12c8a04fca12ac00665d7d73fc80ef4 Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Mon, 19 May 2025 23:56:38 +0000 Subject: [PATCH 11/14] use For --- .../components/union-expression.tsx | 15 ++++++++++++- .../components/union-declaration.test.tsx | 21 ++++++++++++------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/emitter-framework/src/typescript/components/union-expression.tsx b/packages/emitter-framework/src/typescript/components/union-expression.tsx index 63e6255b1d..fecf41d80a 100644 --- a/packages/emitter-framework/src/typescript/components/union-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/union-expression.tsx @@ -120,5 +120,18 @@ function NoneEnvelope(props: NoneEnvelopeProps) { return ; } - return ay.code`{${props.discriminatorPropertyName}: "${String(props.type.name)}"} & ${efRefkey(props.type.type)}`; + const children = [ + + } + /> + , + efRefkey(props.type.type), + ]; + return ( + + {(c) => c} + + ); } diff --git a/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx b/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx index 837623a7c4..973dc8e015 100644 --- a/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx +++ b/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx @@ -141,7 +141,7 @@ describe("Typescript Union Declaration", () => { }); describe("Discriminated Union", () => { - it("renders a discriminated union declaration", async () => { + it("renders a discriminated union", async () => { const { TestUnion } = (await runner.compile(` namespace DemoService; @discriminated @@ -176,7 +176,7 @@ describe("Typescript Union Declaration", () => { }); }); - it("renders a discriminated union declaration with custom properties", async () => { + it("renders a discriminated union with custom properties", async () => { const { TestUnion } = (await runner.compile(` namespace DemoService; @discriminated(#{ discriminatorPropertyName: "dataKind", envelopePropertyName: "data" }) @@ -263,7 +263,7 @@ describe("Typescript Union Declaration", () => { }); describe("Discriminated Union with no envelope", () => { - it("renders named discriminated union declarations", async () => { + it("renders named discriminated union", async () => { const { Pet, Cat, Dog } = (await runner.compile(` namespace DemoService; @@ -295,6 +295,7 @@ describe("Typescript Union Declaration", () => { , ); + assertFileContents( res, d` @@ -306,15 +307,19 @@ describe("Typescript Union Declaration", () => { name: string; bark: boolean; } - type Pet = {dataKind: "cat"} & Cat | {dataKind: "dog"} & Dog; + type Pet = { + dataKind: "cat" + } & Cat | { + dataKind: "dog" + } & Dog; `, ); }); - it("renders a discriminated union declaration with no envelope", async () => { + it("renders anonymous discriminated union", async () => { const { TestUnion } = (await runner.compile(` namespace DemoService; - @discriminated(#{ envelope: "none" }) + @discriminated(#{ envelope: "none", discriminatorPropertyName: "dataKind" }) @test union TestUnion { one: { oneItem: true }, two: { secondItem: false } @@ -333,10 +338,10 @@ describe("Typescript Union Declaration", () => { res, d` type TestUnion = { - kind: "one"; + dataKind: "one"; oneItem: true; } | { - kind: "two"; + dataKind: "two"; secondItem: false; }; `, From 8acbb596762179a9fa6ebf810d39e2865fbdae98 Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Tue, 20 May 2025 00:02:19 +0000 Subject: [PATCH 12/14] comment --- .../src/typescript/components/union-expression.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/emitter-framework/src/typescript/components/union-expression.tsx b/packages/emitter-framework/src/typescript/components/union-expression.tsx index fecf41d80a..e8c17eb832 100644 --- a/packages/emitter-framework/src/typescript/components/union-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/union-expression.tsx @@ -106,7 +106,7 @@ function NoneEnvelope(props: NoneEnvelopeProps) { "Expected all union variants to be models when using a discriminated union with no envelope", ); - // This is an anonymous type, so we render its properties along the discriminator property + // Render anonymous models as a set of properties + the discriminator if ($.model.isExpresion(props.type.type)) { const model = $.model.create({ properties: { @@ -120,6 +120,7 @@ function NoneEnvelope(props: NoneEnvelopeProps) { return ; } + // Render named models as an intersection of the model + the discriminator const children = [ Date: Tue, 20 May 2025 00:03:18 +0000 Subject: [PATCH 13/14] indent --- .../components/union-declaration.test.tsx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx b/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx index 973dc8e015..dae3c9ead5 100644 --- a/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx +++ b/packages/emitter-framework/test/typescript/components/union-declaration.test.tsx @@ -243,22 +243,22 @@ describe("Typescript Union Declaration", () => { assertFileContents( res, d` - interface Cat { - name: string; - meow: boolean; - } - interface Dog { - name: string; - bark: boolean; - } - type Pet = { - kind: "cat"; - value: Cat; - } | { - kind: "dog"; - value: Dog; - }; - `, + interface Cat { + name: string; + meow: boolean; + } + interface Dog { + name: string; + bark: boolean; + } + type Pet = { + kind: "cat"; + value: Cat; + } | { + kind: "dog"; + value: Dog; + }; + `, ); }); From 68a17ff5e0ff573faa00af413894737f581c76c8 Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Tue, 20 May 2025 01:07:11 +0000 Subject: [PATCH 14/14] List --- .../components/union-expression.tsx | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/emitter-framework/src/typescript/components/union-expression.tsx b/packages/emitter-framework/src/typescript/components/union-expression.tsx index e8c17eb832..94b1000420 100644 --- a/packages/emitter-framework/src/typescript/components/union-expression.tsx +++ b/packages/emitter-framework/src/typescript/components/union-expression.tsx @@ -120,19 +120,15 @@ function NoneEnvelope(props: NoneEnvelopeProps) { return ; } - // Render named models as an intersection of the model + the discriminator - const children = [ - - } - /> - , - efRefkey(props.type.type), - ]; return ( - - {(c) => c} - + + + } + /> + + <>{efRefkey(props.type.type)} + ); }