Skip to content

Commit fb0cfd4

Browse files
committed
feat(http): support @doc on unions for response descriptions
Allows using @doc on named unions to provide a unified description for all responses in that union, regardless of individual variant descriptions. This gives users explicit control over response descriptions when multiple response types share the same status code. Example: @doc("The resource conflicts with an existing resource") union Conflict { DuplicateName: DuplicateNameError; DuplicateId: DuplicateIdError; } op createResource(): Resource | Conflict; The union's @doc takes precedence over individual variant descriptions. For nested unions, the innermost union's @doc takes precedence, allowing fine-grained control over different groups of responses. This provides explicit control for cases where the default behavior (using the first variant's description) is not sufficient. Future enhancements could include intelligent description combining when variants have different descriptions but no union @doc is specified.
1 parent 2af7c26 commit fb0cfd4

File tree

3 files changed

+79
-4
lines changed

3 files changed

+79
-4
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@typespec/http"
5+
---
6+
7+
support @doc on unions for response descriptions

packages/http/src/responses.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,24 @@ export function getResponsesForOperation(
3434
const responses = new ResponseIndex();
3535
const tk = $(program);
3636
if (tk.union.is(responseType) && !tk.union.getDiscriminatedUnion(responseType)) {
37+
// Check if the union itself has a @doc to use as the response description
38+
const unionDescription = getDoc(program, responseType);
3739
for (const option of responseType.variants.values()) {
3840
if (isNullType(option.type)) {
3941
// TODO how should we treat this? https://github.com/microsoft/typespec/issues/356
4042
continue;
4143
}
42-
processResponseType(program, diagnostics, operation, responses, option.type);
44+
processResponseType(
45+
program,
46+
diagnostics,
47+
operation,
48+
responses,
49+
option.type,
50+
unionDescription,
51+
);
4352
}
4453
} else {
45-
processResponseType(program, diagnostics, operation, responses, responseType);
54+
processResponseType(program, diagnostics, operation, responses, responseType, undefined);
4655
}
4756

4857
return diagnostics.wrap(responses.values());
@@ -81,6 +90,7 @@ function processResponseType(
8190
operation: Operation,
8291
responses: ResponseIndex,
8392
responseType: Type,
93+
parentDescription?: string,
8494
) {
8595
const tk = $(program);
8696

@@ -89,11 +99,20 @@ function processResponseType(
8999
// or when unions are nested (e.g., a union variant is itself a union).
90100
// Each variant will be processed separately to extract its status codes and responses.
91101
if (tk.union.is(responseType) && !tk.union.getDiscriminatedUnion(responseType)) {
102+
// Check if this nested union has its own @doc, otherwise inherit parent's description
103+
const unionDescription = getDoc(program, responseType) ?? parentDescription;
92104
for (const option of responseType.variants.values()) {
93105
if (isNullType(option.type)) {
94106
continue;
95107
}
96-
processResponseType(program, diagnostics, operation, responses, option.type);
108+
processResponseType(
109+
program,
110+
diagnostics,
111+
operation,
112+
responses,
113+
option.type,
114+
unionDescription,
115+
);
97116
}
98117
return;
99118
}
@@ -132,7 +151,14 @@ function processResponseType(
132151
const response: HttpOperationResponse = responses.get(statusCode) ?? {
133152
statusCodes: statusCode,
134153
type: responseType,
135-
description: getResponseDescription(program, operation, responseType, statusCode, metadata),
154+
description: getResponseDescription(
155+
program,
156+
operation,
157+
responseType,
158+
statusCode,
159+
metadata,
160+
parentDescription,
161+
),
136162
responses: [],
137163
};
138164

@@ -223,7 +249,13 @@ function getResponseDescription(
223249
responseType: Type,
224250
statusCode: HttpStatusCodes[number],
225251
metadata: HttpProperty[],
252+
parentDescription?: string,
226253
): string | undefined {
254+
// If a parent union provided a description, use that first
255+
if (parentDescription) {
256+
return parentDescription;
257+
}
258+
227259
// NOTE: If the response type is an envelope and not the same as the body
228260
// type, then use its @doc as the response description. However, if the
229261
// response type is the same as the body type, then use the default status

packages/openapi3/test/response-descriptions.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,40 @@ worksFor(supportedVersions, ({ openApiFor }) => {
103103
strictEqual(res.paths["/"].get.responses["401"].description, "Model B");
104104
strictEqual(res.paths["/"].get.responses["403"].description, "Model C");
105105
});
106+
107+
it("uses union's @doc when specified on the union itself", async () => {
108+
const res = await openApiFor(
109+
`
110+
@doc("Foo model") model Foo { @statusCode _: 409 }
111+
@doc("Bar model") model Bar { @statusCode _: 409 }
112+
@doc("The resource conflicts with an existing resource")
113+
union Conflict { Foo: Foo; Bar: Bar };
114+
op read(): { @statusCode _: 200, content: string } | Conflict;
115+
`,
116+
);
117+
strictEqual(res.paths["/"].get.responses["200"].description, "The request has succeeded.");
118+
strictEqual(
119+
res.paths["/"].get.responses["409"].description,
120+
"The resource conflicts with an existing resource",
121+
);
122+
});
123+
124+
it("nested union's @doc takes precedence over parent union's @doc", async () => {
125+
const res = await openApiFor(
126+
`
127+
@doc("Model A") model A { @statusCode _: 400 }
128+
@doc("Model B") model B { @statusCode _: 401 }
129+
@doc("Model C") model C { @statusCode _: 403 }
130+
@doc("Inner authentication errors")
131+
union Inner { A: A; B: B };
132+
@doc("All error responses")
133+
union Outer { inner: Inner; C: C };
134+
op read(): { @statusCode _: 200, content: string } | Outer;
135+
`,
136+
);
137+
strictEqual(res.paths["/"].get.responses["200"].description, "The request has succeeded.");
138+
strictEqual(res.paths["/"].get.responses["400"].description, "Inner authentication errors");
139+
strictEqual(res.paths["/"].get.responses["401"].description, "Inner authentication errors");
140+
strictEqual(res.paths["/"].get.responses["403"].description, "All error responses");
141+
});
106142
});

0 commit comments

Comments
 (0)