microsoft/typespec
Publicmirrored from https://github.com/microsoft/typespecAvailable
packages/http/src/responses.ts
245lines · modecode
| 1 | import { |
| 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"; |
| 17 | import { getContentTypes, isContentTypeHeader } from "./content-types.js"; |
| 18 | import { |
| 19 | getHeaderFieldName, |
| 20 | getStatusCodeDescription, |
| 21 | getStatusCodes, |
| 22 | isBody, |
| 23 | isHeader, |
| 24 | isStatusCode, |
| 25 | } from "./decorators.js"; |
| 26 | import { createDiagnostic } from "./lib.js"; |
| 27 | import { gatherMetadata, isApplicableMetadata, Visibility } from "./metadata.js"; |
| 28 | import { HttpOperationResponse } from "./types.js"; |
| 29 | |
| 30 | /** |
| 31 | * Get the responses for a given operation. |
| 32 | */ |
| 33 | export 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 | |
| 55 | function 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 | */ |
| 124 | function 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 | */ |
| 151 | function 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 | */ |
| 168 | function 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 | |
| 182 | function 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 | |
| 224 | function 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 | |