Skip to content

Commit 2c185d5

Browse files
authored
[emitter-framework] Render discriminated unions correctly (microsoft#7369)
This pull request enhances the handling of discriminated unions in the `@typespec/emitter-framework` package. It introduces new rendering logic for discriminated unions, updates existing components to support these changes, and improves test coverage to validate the new functionality. ### Enhancements to discriminated union rendering: * Added new rendering logic for discriminated unions, including support for "object" and "none" envelope styles, through the `ObjectEnvelope` and `NoneEnvelope` functions in `union-expression.tsx`. These handle different discriminator configurations and ensure proper rendering of union variants. [[1]](diffhunk://#diff-400029c681b8e2281a1bde7777af749a18d635dcf9ddaa4a6c9cb1bd86301fe7L22-R27) [[2]](diffhunk://#diff-400029c681b8e2281a1bde7777af749a18d635dcf9ddaa4a6c9cb1bd86301fe7R38-R127) ### Updates to existing components: * Modified the `InterfaceBody` component in `interface-declaration.tsx` to include a semicolon when rendering type members, ensuring consistency with TypeScript syntax. Found in passing and validated via the new tests ### Improvements to test coverage: * Refactored and expanded tests in `union-declaration.test.tsx` using the new testing patterns Fixes microsoft#7174
1 parent c45129e commit 2c185d5

File tree

4 files changed

+408
-111
lines changed

4 files changed

+408
-111
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: fix
3+
packages:
4+
- "@typespec/emitter-framework"
5+
---
6+
7+
Render discriminated unions correctly

packages/emitter-framework/src/typescript/components/interface-declaration.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ function InterfaceBody(props: TypedInterfaceDeclarationProps): Children {
154154

155155
return (
156156
<>
157-
<ay.For each={validTypeMembers} line {...enderProp}>
157+
<ay.For each={validTypeMembers} semicolon line {...enderProp}>
158158
{(typeMember) => {
159159
return <InterfaceMember type={typeMember} />;
160160
}}

packages/emitter-framework/src/typescript/components/union-expression.tsx

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as ay from "@alloy-js/core";
22
import { Children } from "@alloy-js/core";
33
import * as ts from "@alloy-js/typescript";
4-
import { Enum, EnumMember, Union, UnionVariant } from "@typespec/compiler";
4+
import { compilerAssert, Enum, EnumMember, Union, UnionVariant } from "@typespec/compiler";
55
import { useTsp } from "../../core/context/tsp-context.js";
6+
import { efRefkey } from "../utils/refkey.js";
67
import { TypeExpression } from "./type-expression.jsx";
78

89
export interface UnionExpressionProps {
@@ -19,11 +20,30 @@ export function UnionExpression({ type, children }: UnionExpressionProps) {
1920

2021
const variants = (
2122
<ay.For joiner={" | "} each={items}>
22-
{(_, value) => {
23-
if ($.enumMember.is(value)) {
24-
return <ts.ValueExpression jsValue={value.value ?? value.name} />;
25-
} else {
26-
return <TypeExpression type={value.type} />;
23+
{(_, type) => {
24+
if ($.enumMember.is(type)) {
25+
return <ts.ValueExpression jsValue={type.value ?? type.name} />;
26+
}
27+
28+
const discriminatedUnion = $.union.getDiscriminatedUnion(type.union);
29+
switch (discriminatedUnion?.options.envelope) {
30+
case "object":
31+
return (
32+
<ObjectEnvelope
33+
discriminatorPropertyName={discriminatedUnion.options.discriminatorPropertyName}
34+
envelopePropertyName={discriminatedUnion.options.envelopePropertyName}
35+
type={type}
36+
/>
37+
);
38+
case "none":
39+
return (
40+
<NoneEnvelope
41+
discriminatorPropertyName={discriminatedUnion.options.discriminatorPropertyName}
42+
type={type}
43+
/>
44+
);
45+
default:
46+
return <TypeExpression type={type.type} />;
2747
}
2848
}}
2949
</ay.For>
@@ -32,10 +52,83 @@ export function UnionExpression({ type, children }: UnionExpressionProps) {
3252
if (children || (Array.isArray(children) && children.length)) {
3353
return (
3454
<>
35-
{variants} {` | ${children}`}
55+
{variants} {`| ${children}`}
3656
</>
3757
);
3858
}
3959

4060
return variants;
4161
}
62+
63+
interface ObjectEnvelopeProps {
64+
type: UnionVariant;
65+
discriminatorPropertyName: string;
66+
envelopePropertyName: string;
67+
}
68+
69+
/**
70+
* Renders a discriminated union with "object" envelope style
71+
* where model properties are nested inside an envelope
72+
*/
73+
function ObjectEnvelope(props: ObjectEnvelopeProps) {
74+
const { $ } = useTsp();
75+
76+
const envelope = $.model.create({
77+
properties: {
78+
[props.discriminatorPropertyName]: $.modelProperty.create({
79+
name: props.discriminatorPropertyName,
80+
type: $.literal.createString(props.type.name as string),
81+
}),
82+
[props.envelopePropertyName]: $.modelProperty.create({
83+
name: props.envelopePropertyName,
84+
type: props.type.type,
85+
}),
86+
},
87+
});
88+
89+
return <TypeExpression type={envelope} />;
90+
}
91+
92+
interface NoneEnvelopeProps {
93+
type: UnionVariant;
94+
discriminatorPropertyName: string;
95+
}
96+
97+
/**
98+
* Renders a discriminated union with "none" envelope style
99+
* where discriminator property sits alongside model properties
100+
*/
101+
function NoneEnvelope(props: NoneEnvelopeProps) {
102+
const { $ } = useTsp();
103+
104+
compilerAssert(
105+
$.model.is(props.type.type),
106+
"Expected all union variants to be models when using a discriminated union with no envelope",
107+
);
108+
109+
// Render anonymous models as a set of properties + the discriminator
110+
if ($.model.isExpresion(props.type.type)) {
111+
const model = $.model.create({
112+
properties: {
113+
[props.discriminatorPropertyName]: $.modelProperty.create({
114+
name: props.discriminatorPropertyName,
115+
type: $.literal.createString(props.type.name as string),
116+
}),
117+
...Object.fromEntries(props.type.type.properties),
118+
},
119+
});
120+
return <TypeExpression type={model} />;
121+
}
122+
123+
return (
124+
<ay.List joiner={" & "}>
125+
<ts.ObjectExpression>
126+
<ts.ObjectProperty
127+
name={props.discriminatorPropertyName}
128+
value={<ts.ValueExpression jsValue={props.type.name} />}
129+
/>
130+
</ts.ObjectExpression>
131+
<>{efRefkey(props.type.type)}</>
132+
</ay.List>
133+
);
134+
}

0 commit comments

Comments
 (0)