microsoft/typespec

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e26e7826068edd7ad2659a4a2ea74397dcdd1ea6

Branches

Tags

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

Clone

HTTPS

Download ZIP

packages/http/src/responses.ts

245lines · modecode

1import {
2 createDiagnosticCollector,
3 Diagnostic,
4 DiagnosticCollector,
5 getDoc,
6 isArrayModelType,
7 isErrorModel,
8 isNullType,
9 isVoidType,
10 Model,
11 ModelProperty,
12 Operation,
13 Program,
14 Type,
15 walkPropertiesInherited,
16} from "@typespec/compiler";
17import { getContentTypes, isContentTypeHeader } from "./content-types.js";
18import {
19 getHeaderFieldName,
20 getStatusCodeDescription,
21 getStatusCodes,
22 isBody,
23 isHeader,
24 isStatusCode,
25} from "./decorators.js";
26import { createDiagnostic } from "./lib.js";
27import { gatherMetadata, isApplicableMetadata, Visibility } from "./metadata.js";
28import { HttpOperationResponse } from "./types.js";
29
30/**
31 * Get the responses for a given operation.
32 */
33export function getResponsesForOperation(
34 program: Program,
35 operation: Operation
36): [HttpOperationResponse[], readonly Diagnostic[]] {
37 const diagnostics = createDiagnosticCollector();
38 const responseType = operation.returnType;
39 const responses: Record<string | symbol, HttpOperationResponse> = {};
40 if (responseType.kind === "Union") {
41 for (const option of responseType.variants.values()) {
42 if (isNullType(option.type)) {
43 // TODO how should we treat this? https://github.com/microsoft/typespec/issues/356
44 continue;
45 }
46 processResponseType(program, diagnostics, responses, option.type);
47 }
48 } else {
49 processResponseType(program, diagnostics, responses, responseType);
50 }
51
52 return diagnostics.wrap(Object.values(responses));
53}
54
55function processResponseType(
56 program: Program,
57 diagnostics: DiagnosticCollector,
58 responses: Record<string, HttpOperationResponse>,
59 responseType: Type
60) {
61 const metadata = gatherMetadata(program, diagnostics, responseType, Visibility.Read);
62
63 // Get explicity defined status codes
64 const statusCodes: Array<string> = getResponseStatusCodes(program, responseType, metadata);
65
66 // Get explicitly defined content types
67 const contentTypes = getResponseContentTypes(program, diagnostics, metadata);
68
69 // Get response headers
70 const headers = getResponseHeaders(program, metadata);
71
72 // Get body
73 let bodyType = getResponseBody(program, diagnostics, responseType, metadata);
74
75 // If there is no explicit status code, check if it should be 204
76 if (statusCodes.length === 0) {
77 if (bodyType === undefined || isVoidType(bodyType)) {
78 bodyType = undefined;
79 statusCodes.push("204");
80 } else if (isErrorModel(program, responseType)) {
81 statusCodes.push("*");
82 } else {
83 statusCodes.push("200");
84 }
85 }
86
87 // If there is a body but no explicit content types, use application/json
88 if (bodyType && contentTypes.length === 0) {
89 contentTypes.push("application/json");
90 }
91
92 // Put them into currentEndpoint.responses
93 for (const statusCode of statusCodes) {
94 // the first model for this statusCode/content type pair carries the
95 // description for the endpoint. This could probably be improved.
96 const response: HttpOperationResponse = responses[statusCode] ?? {
97 statusCode,
98 type: responseType,
99 description: getResponseDescription(program, responseType, statusCode, bodyType),
100 responses: [],
101 };
102
103 if (bodyType !== undefined) {
104 response.responses.push({ body: { contentTypes: contentTypes, type: bodyType }, headers });
105 } else if (contentTypes.length > 0) {
106 diagnostics.add(
107 createDiagnostic({
108 code: "content-type-ignored",
109 target: responseType,
110 })
111 );
112 } else {
113 response.responses.push({ headers });
114 }
115 responses[statusCode] = response;
116 }
117}
118
119/**
120 * Get explicity defined status codes from response type and metadata
121 * Return is an array of strings, possibly empty, which indicates no explicitly defined status codes.
122 * We do not check for duplicates here -- that will be done by the caller.
123 */
124function getResponseStatusCodes(
125 program: Program,
126 responseType: Type,
127 metadata: Set<ModelProperty>
128): string[] {
129 const codes: string[] = [];
130
131 for (const prop of metadata) {
132 if (isStatusCode(program, prop)) {
133 codes.push(...getStatusCodes(program, prop));
134 }
135 }
136
137 if (responseType.kind === "Model") {
138 for (let t: Model | undefined = responseType; t; t = t.baseModel) {
139 codes.push(...getStatusCodes(program, t));
140 }
141 }
142
143 return codes;
144}
145
146/**
147 * Get explicity defined content-types from response metadata
148 * Return is an array of strings, possibly empty, which indicates no explicitly defined content-type.
149 * We do not check for duplicates here -- that will be done by the caller.
150 */
151function getResponseContentTypes(
152 program: Program,
153 diagnostics: DiagnosticCollector,
154 metadata: Set<ModelProperty>
155): string[] {
156 const contentTypes: string[] = [];
157 for (const prop of metadata) {
158 if (isHeader(program, prop) && isContentTypeHeader(program, prop)) {
159 contentTypes.push(...diagnostics.pipe(getContentTypes(prop)));
160 }
161 }
162 return contentTypes;
163}
164
165/**
166 * Get response headers from response metadata
167 */
168function getResponseHeaders(
169 program: Program,
170 metadata: Set<ModelProperty>
171): Record<string, ModelProperty> {
172 const responseHeaders: Record<string, ModelProperty> = {};
173 for (const prop of metadata) {
174 const headerName = getHeaderFieldName(program, prop);
175 if (isHeader(program, prop) && headerName !== "content-type") {
176 responseHeaders[headerName] = prop;
177 }
178 }
179 return responseHeaders;
180}
181
182function getResponseBody(
183 program: Program,
184 diagnostics: DiagnosticCollector,
185 responseType: Type,
186 metadata: Set<ModelProperty>
187): Type | undefined {
188 // non-model or intrinsic/array model -> response body is response type
189 if (responseType.kind !== "Model" || isArrayModelType(program, responseType)) {
190 return responseType;
191 }
192
193 // look for explicit body
194 let bodyProperty: ModelProperty | undefined;
195 for (const property of metadata) {
196 if (isBody(program, property)) {
197 if (bodyProperty) {
198 diagnostics.add(createDiagnostic({ code: "duplicate-body", target: property }));
199 } else {
200 bodyProperty = property;
201 }
202 }
203 }
204 if (bodyProperty) {
205 return bodyProperty.type;
206 }
207
208 // Without an explicit body, response type is response model itself if
209 // there it has at least one non-metadata property, if it is an empty object or if it has derived
210 // models
211 if (responseType.derivedModels.length > 0 || responseType.properties.size === 0) {
212 return responseType;
213 }
214 for (const property of walkPropertiesInherited(responseType)) {
215 if (!isApplicableMetadata(program, property, Visibility.Read)) {
216 return responseType;
217 }
218 }
219
220 // Otherwise, there is no body
221 return undefined;
222}
223
224function getResponseDescription(
225 program: Program,
226 responseType: Type,
227 statusCode: string,
228 bodyType: Type | undefined
229): string | undefined {
230 // NOTE: If the response type is an envelope and not the same as the body
231 // type, then use its @doc as the response description. However, if the
232 // response type is the same as the body type, then use the default status
233 // code description and don't duplicate the schema description of the body
234 // as the response description. This allows more freedom to change how
235 // TypeSpec is expressed in semantically equivalent ways without causing
236 // the output to change unnecessarily.
237 if (responseType !== bodyType) {
238 const desc = getDoc(program, responseType);
239 if (desc) {
240 return desc;
241 }
242 }
243
244 return getStatusCodeDescription(statusCode);
245}
246