microsoft/typespec
Publicmirrored from https://github.com/microsoft/typespecAvailable
packages/openapi3/src/openapi.ts
1224lines · modecode
| 1 | import { |
| 2 | ArrayType, |
| 3 | checkIfServiceNamespace, |
| 4 | EmitOptionsFor, |
| 5 | EnumMemberType, |
| 6 | EnumType, |
| 7 | getAllTags, |
| 8 | getDoc, |
| 9 | getFormat, |
| 10 | getFriendlyName, |
| 11 | getIntrinsicModelName, |
| 12 | getKnownValues, |
| 13 | getMaxLength, |
| 14 | getMaxValue, |
| 15 | getMinLength, |
| 16 | getMinValue, |
| 17 | getPattern, |
| 18 | getProperty, |
| 19 | getPropertyType, |
| 20 | getServiceHost, |
| 21 | getServiceNamespace, |
| 22 | getServiceNamespaceString, |
| 23 | getServiceTitle, |
| 24 | getServiceVersion, |
| 25 | getSummary, |
| 26 | getVisibility, |
| 27 | isErrorType, |
| 28 | isIntrinsic, |
| 29 | isNumericType, |
| 30 | isSecret, |
| 31 | isStringType, |
| 32 | isTemplate, |
| 33 | ModelType, |
| 34 | ModelTypeProperty, |
| 35 | NamespaceType, |
| 36 | OperationType, |
| 37 | Program, |
| 38 | resolvePath, |
| 39 | Type, |
| 40 | UnionType, |
| 41 | UnionTypeVariant, |
| 42 | } from "@cadl-lang/compiler"; |
| 43 | import { getExtensions, getExternalDocs, getOperationId } from "@cadl-lang/openapi"; |
| 44 | import { |
| 45 | Discriminator, |
| 46 | getAllRoutes, |
| 47 | getContentTypes, |
| 48 | getDiscriminator, |
| 49 | http, |
| 50 | HttpOperationParameter, |
| 51 | HttpOperationParameters, |
| 52 | HttpOperationResponse, |
| 53 | OperationDetails, |
| 54 | } from "@cadl-lang/rest"; |
| 55 | import { getVersionRecords } from "@cadl-lang/versioning"; |
| 56 | import { getOneOf, getRef } from "./decorators.js"; |
| 57 | import { OpenAPILibrary, reportDiagnostic } from "./lib.js"; |
| 58 | import { |
| 59 | OpenAPI3Discriminator, |
| 60 | OpenAPI3Operation, |
| 61 | OpenAPI3Parameter, |
| 62 | OpenAPI3ParameterType, |
| 63 | OpenAPI3Schema, |
| 64 | } from "./types.js"; |
| 65 | |
| 66 | const { |
| 67 | getHeaderFieldName, |
| 68 | getPathParamName, |
| 69 | getQueryParamName, |
| 70 | isStatusCode, |
| 71 | getStatusCodeDescription, |
| 72 | } = http; |
| 73 | |
| 74 | export async function $onEmit(p: Program, emitterOptions?: EmitOptionsFor<OpenAPILibrary>) { |
| 75 | const options: OpenAPIEmitterOptions = { |
| 76 | outputFile: p.compilerOptions.swaggerOutputFile || resolvePath("./openapi.json"), |
| 77 | }; |
| 78 | |
| 79 | const emitter = createOAPIEmitter(p, options); |
| 80 | await emitter.emitOpenAPI(); |
| 81 | } |
| 82 | |
| 83 | // NOTE: These functions aren't meant to be used directly as decorators but as a |
| 84 | // helper functions for other decorators. The security information given here |
| 85 | // will be inserted into the `security` and `securityDefinitions` sections of |
| 86 | // the emitted OpenAPI document. |
| 87 | |
| 88 | const securityDetailsKey = Symbol("securityDetails"); |
| 89 | interface SecurityDetails { |
| 90 | definitions: any; |
| 91 | requirements: any[]; |
| 92 | } |
| 93 | |
| 94 | function getSecurityDetails(program: Program, serviceNamespace: NamespaceType): SecurityDetails { |
| 95 | const definitions = program.stateMap(securityDetailsKey); |
| 96 | if (definitions.has(serviceNamespace)) { |
| 97 | return definitions.get(serviceNamespace)!; |
| 98 | } else { |
| 99 | const details = { definitions: {}, requirements: [] }; |
| 100 | definitions.set(serviceNamespace, details); |
| 101 | return details; |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | function getSecurityRequirements(program: Program, serviceNamespace: NamespaceType) { |
| 106 | return getSecurityDetails(program, serviceNamespace).requirements; |
| 107 | } |
| 108 | |
| 109 | function getSecurityDefinitions(program: Program, serviceNamespace: NamespaceType) { |
| 110 | return getSecurityDetails(program, serviceNamespace).definitions; |
| 111 | } |
| 112 | |
| 113 | export function addSecurityRequirement( |
| 114 | program: Program, |
| 115 | namespace: NamespaceType, |
| 116 | name: string, |
| 117 | scopes: string[] |
| 118 | ): void { |
| 119 | if (!checkIfServiceNamespace(program, namespace)) { |
| 120 | reportDiagnostic(program, { |
| 121 | code: "security-service-namespace", |
| 122 | target: namespace, |
| 123 | }); |
| 124 | } |
| 125 | |
| 126 | const req: any = {}; |
| 127 | req[name] = scopes; |
| 128 | const requirements = getSecurityRequirements(program, namespace); |
| 129 | requirements.push(req); |
| 130 | } |
| 131 | |
| 132 | export function addSecurityDefinition( |
| 133 | program: Program, |
| 134 | namespace: NamespaceType, |
| 135 | name: string, |
| 136 | details: any |
| 137 | ): void { |
| 138 | if (!checkIfServiceNamespace(program, namespace)) { |
| 139 | reportDiagnostic(program, { |
| 140 | code: "security-service-namespace", |
| 141 | target: namespace, |
| 142 | }); |
| 143 | return; |
| 144 | } |
| 145 | |
| 146 | const definitions = getSecurityDefinitions(program, namespace); |
| 147 | definitions[name] = details; |
| 148 | } |
| 149 | |
| 150 | export interface OpenAPIEmitterOptions { |
| 151 | outputFile: string; |
| 152 | } |
| 153 | |
| 154 | function createOAPIEmitter(program: Program, options: OpenAPIEmitterOptions) { |
| 155 | let root: any; |
| 156 | let host: string | undefined; |
| 157 | |
| 158 | // Get the service namespace string for use in name shortening |
| 159 | let serviceNamespace: string | undefined; |
| 160 | let currentPath: any; |
| 161 | let currentEndpoint: OpenAPI3Operation; |
| 162 | |
| 163 | // Keep a list of all Types encountered that need schema definitions |
| 164 | let schemas = new Set<Type>(); |
| 165 | |
| 166 | // Map model properties that represent shared parameters to their parameter |
| 167 | // definition that will go in #/components/parameters. Inlined parameters do not go in |
| 168 | // this map. |
| 169 | let params: Map<ModelTypeProperty, any>; |
| 170 | |
| 171 | // De-dupe the per-endpoint tags that will be added into the #/tags |
| 172 | let tags: Set<string>; |
| 173 | |
| 174 | return { emitOpenAPI }; |
| 175 | |
| 176 | function initializeEmitter(serviceNamespaceType: NamespaceType, version?: string) { |
| 177 | root = { |
| 178 | openapi: "3.0.0", |
| 179 | info: { |
| 180 | title: getServiceTitle(program), |
| 181 | version: version ?? getServiceVersion(program), |
| 182 | description: getDoc(program, serviceNamespaceType), |
| 183 | }, |
| 184 | externalDocs: getExternalDocs(program, serviceNamespaceType), |
| 185 | tags: [], |
| 186 | paths: {}, |
| 187 | components: { |
| 188 | parameters: {}, |
| 189 | requestBodies: {}, |
| 190 | responses: {}, |
| 191 | schemas: {}, |
| 192 | examples: {}, |
| 193 | securitySchemes: {}, |
| 194 | }, |
| 195 | }; |
| 196 | host = getServiceHost(program); |
| 197 | if (host) { |
| 198 | root.servers = [ |
| 199 | { |
| 200 | url: "https://" + host, |
| 201 | }, |
| 202 | ]; |
| 203 | } |
| 204 | |
| 205 | serviceNamespace = getServiceNamespaceString(program); |
| 206 | currentPath = root.paths; |
| 207 | schemas = new Set(); |
| 208 | params = new Map(); |
| 209 | tags = new Set(); |
| 210 | } |
| 211 | |
| 212 | async function emitOpenAPI() { |
| 213 | const serviceNs = getServiceNamespace(program); |
| 214 | if (!serviceNs) { |
| 215 | return; |
| 216 | } |
| 217 | const versions = getVersionRecords(program, serviceNs); |
| 218 | for (const record of versions) { |
| 219 | if (record.version) { |
| 220 | record.projections.push({ |
| 221 | projectionName: "atVersion", |
| 222 | arguments: [record.version], |
| 223 | }); |
| 224 | } |
| 225 | |
| 226 | if (record.projections.length > 0) { |
| 227 | program.enableProjections(record.projections); |
| 228 | } |
| 229 | |
| 230 | await emitOpenAPIFromVersion(serviceNs, record.version); |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | async function emitOpenAPIFromVersion(serviceNamespace: NamespaceType, version?: string) { |
| 235 | initializeEmitter(serviceNamespace, version); |
| 236 | try { |
| 237 | getAllRoutes(program).forEach(emitOperation); |
| 238 | emitReferences(); |
| 239 | emitTags(); |
| 240 | |
| 241 | // Clean up empty entries |
| 242 | for (const elem of Object.keys(root.components)) { |
| 243 | if (Object.keys(root.components[elem]).length === 0) { |
| 244 | delete root.components[elem]; |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | if (!program.compilerOptions.noEmit && !program.hasError()) { |
| 249 | // Write out the OpenAPI document to the output path |
| 250 | const outPath = version |
| 251 | ? resolvePath(options.outputFile.replace(".json", `.${version}.json`)) |
| 252 | : resolvePath(options.outputFile); |
| 253 | |
| 254 | await program.host.writeFile(outPath, prettierOutput(JSON.stringify(root, null, 2))); |
| 255 | } |
| 256 | } catch (err) { |
| 257 | if (err instanceof ErrorTypeFoundError) { |
| 258 | // Return early, there must be a parse error if an ErrorType was |
| 259 | // inserted into the Cadl output |
| 260 | return; |
| 261 | } else { |
| 262 | throw err; |
| 263 | } |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | function emitOperation(operation: OperationDetails): void { |
| 268 | const { path: fullPath, operation: op, groupName, verb, parameters } = operation; |
| 269 | |
| 270 | // If path contains a query string, issue msg and don't emit this endpoint |
| 271 | if (fullPath.indexOf("?") > 0) { |
| 272 | reportDiagnostic(program, { code: "path-query", target: op }); |
| 273 | return; |
| 274 | } |
| 275 | |
| 276 | if (!root.paths[fullPath]) { |
| 277 | root.paths[fullPath] = {}; |
| 278 | } |
| 279 | |
| 280 | currentPath = root.paths[fullPath]; |
| 281 | if (!currentPath[verb]) { |
| 282 | currentPath[verb] = {}; |
| 283 | } |
| 284 | currentEndpoint = currentPath[verb]; |
| 285 | |
| 286 | const currentTags = getAllTags(program, op); |
| 287 | if (currentTags) { |
| 288 | currentEndpoint.tags = currentTags; |
| 289 | for (const tag of currentTags) { |
| 290 | // Add to root tags if not already there |
| 291 | tags.add(tag); |
| 292 | } |
| 293 | } |
| 294 | |
| 295 | const operationId = getOperationId(program, op); |
| 296 | if (operationId) { |
| 297 | currentEndpoint.operationId = operationId; |
| 298 | } else { |
| 299 | // Synthesize an operation ID |
| 300 | currentEndpoint.operationId = (groupName.length > 0 ? `${groupName}_` : "") + op.name; |
| 301 | } |
| 302 | applyExternalDocs(op, currentEndpoint); |
| 303 | |
| 304 | // Set up basic endpoint fields |
| 305 | currentEndpoint.summary = getSummary(program, op); |
| 306 | currentEndpoint.description = getDoc(program, op); |
| 307 | currentEndpoint.parameters = []; |
| 308 | currentEndpoint.responses = {}; |
| 309 | |
| 310 | emitEndpointParameters(parameters.parameters); |
| 311 | emitRequestBody(op, op.parameters, parameters); |
| 312 | emitResponses(operation.responses); |
| 313 | |
| 314 | attachExtensions(program, op, currentEndpoint); |
| 315 | } |
| 316 | |
| 317 | function emitResponses(responses: HttpOperationResponse[]) { |
| 318 | for (const response of responses) { |
| 319 | emitResponseObject(response); |
| 320 | } |
| 321 | } |
| 322 | |
| 323 | function isBinaryPayload(body: Type, contentType: string) { |
| 324 | return ( |
| 325 | body.kind === "Model" && |
| 326 | body.name === "bytes" && |
| 327 | contentType !== "application/json" && |
| 328 | contentType !== "text/plain" |
| 329 | ); |
| 330 | } |
| 331 | |
| 332 | function getOpenAPIStatuscode(response: HttpOperationResponse): string { |
| 333 | switch (response.statusCode) { |
| 334 | case "*": |
| 335 | return "default"; |
| 336 | default: |
| 337 | return response.statusCode; |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | function emitResponseObject(response: Readonly<HttpOperationResponse>) { |
| 342 | const statusCode = getOpenAPIStatuscode(response); |
| 343 | const openapiResponse = currentEndpoint.responses[statusCode] ?? { |
| 344 | description: response.description ?? getResponseDescriptionForStatusCode(statusCode), |
| 345 | }; |
| 346 | |
| 347 | for (const data of response.responses) { |
| 348 | if (data.headers && Object.keys(data.headers).length > 0) { |
| 349 | openapiResponse.headers ??= {}; |
| 350 | // OpenAPI can't represent different headers per content type. |
| 351 | // So we merge headers here, and report any duplicates. |
| 352 | // It may be possible in principle to not error for identically declared |
| 353 | // headers. |
| 354 | for (const [key, value] of Object.entries(data.headers)) { |
| 355 | if (openapiResponse.headers[key]) { |
| 356 | reportDiagnostic(program, { |
| 357 | code: "duplicate-header", |
| 358 | format: { header: key }, |
| 359 | target: response.type, |
| 360 | }); |
| 361 | continue; |
| 362 | } |
| 363 | openapiResponse.headers[key] = getResponseHeader(value); |
| 364 | } |
| 365 | } |
| 366 | |
| 367 | if (data.body !== undefined) { |
| 368 | openapiResponse.content ??= {}; |
| 369 | for (const contentType of data.body.contentTypes) { |
| 370 | const isBinary = isBinaryPayload(data.body.type, contentType); |
| 371 | const schema = isBinary |
| 372 | ? { type: "string", format: "binary" } |
| 373 | : getSchemaOrRef(data.body.type); |
| 374 | openapiResponse.content[contentType] = { schema }; |
| 375 | } |
| 376 | } |
| 377 | } |
| 378 | |
| 379 | currentEndpoint.responses[statusCode] = openapiResponse; |
| 380 | } |
| 381 | |
| 382 | function getResponseDescriptionForStatusCode(statusCode: string) { |
| 383 | if (statusCode === "default") { |
| 384 | return "An unexpected error response"; |
| 385 | } |
| 386 | return getStatusCodeDescription(statusCode) ?? "unknown"; |
| 387 | } |
| 388 | |
| 389 | function getResponseHeader(prop: ModelTypeProperty) { |
| 390 | const header: any = {}; |
| 391 | populateParameter(header, prop, "header"); |
| 392 | delete header.in; |
| 393 | delete header.name; |
| 394 | delete header.required; |
| 395 | return header; |
| 396 | } |
| 397 | |
| 398 | function getSchemaOrRef(type: Type): any { |
| 399 | const refUrl = getRef(program, type); |
| 400 | if (refUrl) { |
| 401 | return { |
| 402 | $ref: refUrl, |
| 403 | }; |
| 404 | } |
| 405 | |
| 406 | if (type.kind === "Model" && type.name === getIntrinsicModelName(program, type)) { |
| 407 | // if the model is one of the Cadl Intrinsic type. |
| 408 | // it's a base Cadl "primitive" that corresponds directly to an OpenAPI |
| 409 | // primitive. In such cases, we don't want to emit a ref and instead just |
| 410 | // emit the base type directly. |
| 411 | const builtIn = mapCadlIntrinsicModelToOpenAPI(type); |
| 412 | if (builtIn !== undefined) { |
| 413 | return builtIn; |
| 414 | } |
| 415 | } |
| 416 | |
| 417 | if (type.kind === "String" || type.kind === "Number" || type.kind === "Boolean") { |
| 418 | // For literal types, we just want to emit them directly as well. |
| 419 | return mapCadlTypeToOpenAPI(type); |
| 420 | } |
| 421 | const name = getTypeNameForSchemaProperties(type); |
| 422 | if (!isRefSafeName(name)) { |
| 423 | // Schema's name is not reference-able in OpenAPI so we inline it. |
| 424 | // This will usually happen with instantiated/anonymous types, but could also |
| 425 | // happen if Cadl identifier uses characters that are problematic for OpenAPI. |
| 426 | // Users will have to rename / alias type to have it get ref'ed. |
| 427 | const schema = getSchemaForType(type); |
| 428 | |
| 429 | if (schema === undefined && isErrorType(type)) { |
| 430 | // Exit early so that syntax errors are exposed. This error will |
| 431 | // be caught and handled in emitOpenAPI. |
| 432 | throw new ErrorTypeFoundError(); |
| 433 | } |
| 434 | |
| 435 | // helps to read output and correlate to Cadl |
| 436 | if (schema) { |
| 437 | schema["x-cadl-name"] = name; |
| 438 | } |
| 439 | return schema; |
| 440 | } else { |
| 441 | const placeholder = { |
| 442 | $ref: "#/components/schemas/" + name, |
| 443 | }; |
| 444 | schemas.add(type); |
| 445 | return placeholder; |
| 446 | } |
| 447 | } |
| 448 | |
| 449 | function getParamPlaceholder(property: ModelTypeProperty) { |
| 450 | let spreadParam = false; |
| 451 | |
| 452 | if (property.sourceProperty) { |
| 453 | // chase our sources all the way back to the first place this property |
| 454 | // was defined. |
| 455 | spreadParam = true; |
| 456 | property = property.sourceProperty; |
| 457 | while (property.sourceProperty) { |
| 458 | property = property.sourceProperty; |
| 459 | } |
| 460 | } |
| 461 | |
| 462 | const refUrl = getRef(program, property); |
| 463 | if (refUrl) { |
| 464 | return { |
| 465 | $ref: refUrl, |
| 466 | }; |
| 467 | } |
| 468 | |
| 469 | if (params.has(property)) { |
| 470 | return params.get(property); |
| 471 | } |
| 472 | |
| 473 | const placeholder = {}; |
| 474 | // only parameters inherited by spreading or from interface are shared in #/parameters |
| 475 | // bt: not sure about the interface part of this comment? |
| 476 | |
| 477 | if (spreadParam) { |
| 478 | params.set(property, placeholder); |
| 479 | } |
| 480 | |
| 481 | return placeholder; |
| 482 | } |
| 483 | |
| 484 | function emitEndpointParameters(parameters: HttpOperationParameter[]) { |
| 485 | for (const { type, name, param } of parameters) { |
| 486 | // If param is a global parameter, just skip it |
| 487 | if (params.has(param)) { |
| 488 | currentEndpoint.parameters.push(params.get(param)); |
| 489 | continue; |
| 490 | } |
| 491 | |
| 492 | switch (type) { |
| 493 | case "path": |
| 494 | emitParameter(param, "path"); |
| 495 | break; |
| 496 | case "query": |
| 497 | emitParameter(param, "query"); |
| 498 | break; |
| 499 | case "header": |
| 500 | if (name !== "content-type") { |
| 501 | emitParameter(param, "header"); |
| 502 | } |
| 503 | break; |
| 504 | } |
| 505 | } |
| 506 | } |
| 507 | |
| 508 | function emitRequestBody( |
| 509 | op: OperationType, |
| 510 | parent: ModelType | undefined, |
| 511 | parameters: HttpOperationParameters |
| 512 | ) { |
| 513 | if (parameters.body === undefined) { |
| 514 | return; |
| 515 | } |
| 516 | |
| 517 | const bodyParam = parameters.body; |
| 518 | const bodyType = bodyParam.type; |
| 519 | |
| 520 | const requestBody: any = { |
| 521 | description: getDoc(program, bodyParam), |
| 522 | content: {}, |
| 523 | }; |
| 524 | |
| 525 | const contentTypeParam = parameters.parameters.find( |
| 526 | (p) => p.type === "header" && p.name === "content-type" |
| 527 | ); |
| 528 | const contentTypes = contentTypeParam |
| 529 | ? getContentTypes(program, contentTypeParam.param) |
| 530 | : ["application/json"]; |
| 531 | for (const contentType of contentTypes) { |
| 532 | const isBinary = isBinaryPayload(bodyType, contentType); |
| 533 | const bodySchema = isBinary ? { type: "string", format: "binary" } : getSchemaOrRef(bodyType); |
| 534 | const contentEntry: any = { |
| 535 | schema: bodySchema, |
| 536 | }; |
| 537 | requestBody.content[contentType] = contentEntry; |
| 538 | } |
| 539 | |
| 540 | currentEndpoint.requestBody = requestBody; |
| 541 | } |
| 542 | |
| 543 | function emitParameter(param: ModelTypeProperty, kind: OpenAPI3ParameterType) { |
| 544 | const ph = getParamPlaceholder(param); |
| 545 | currentEndpoint.parameters.push(ph); |
| 546 | |
| 547 | // If the parameter already has a $ref, don't bother populating it |
| 548 | if (!("$ref" in ph)) { |
| 549 | populateParameter(ph, param, kind); |
| 550 | } |
| 551 | } |
| 552 | |
| 553 | function populateParameter( |
| 554 | ph: OpenAPI3Parameter, |
| 555 | param: ModelTypeProperty, |
| 556 | kind: OpenAPI3ParameterType |
| 557 | ) { |
| 558 | ph.name = param.name; |
| 559 | ph.in = kind; |
| 560 | ph.required = !param.optional; |
| 561 | ph.description = getDoc(program, param); |
| 562 | |
| 563 | // Apply decorators to the schema for the parameter. |
| 564 | const schema = applyIntrinsicDecorators(param, getSchemaForType(param.type)); |
| 565 | if (param.type.kind === "Array") { |
| 566 | schema.items = getSchemaForType(param.type.elementType); |
| 567 | } |
| 568 | if (param.default) { |
| 569 | schema.default = getDefaultValue(param.default); |
| 570 | } |
| 571 | attachExtensions(program, param, ph); |
| 572 | // Description is already provided in the parameter itself. |
| 573 | delete schema.description; |
| 574 | ph.schema = schema; |
| 575 | } |
| 576 | |
| 577 | function emitReferences() { |
| 578 | for (const [property, param] of params) { |
| 579 | const key = getParameterKey(property, param); |
| 580 | |
| 581 | root.components.parameters[key] = { ...param }; |
| 582 | |
| 583 | for (const key of Object.keys(param)) { |
| 584 | delete param[key]; |
| 585 | } |
| 586 | |
| 587 | param["$ref"] = "#/components/parameters/" + key; |
| 588 | } |
| 589 | |
| 590 | for (const type of schemas) { |
| 591 | const name = getTypeNameForSchemaProperties(type); |
| 592 | const schemaForType = getSchemaForType(type); |
| 593 | if (schemaForType) { |
| 594 | root.components.schemas[name] = schemaForType; |
| 595 | } |
| 596 | } |
| 597 | } |
| 598 | |
| 599 | function emitTags() { |
| 600 | for (const tag of tags) { |
| 601 | root.tags.push({ name: tag }); |
| 602 | } |
| 603 | } |
| 604 | |
| 605 | function getParameterKey(property: ModelTypeProperty, param: any) { |
| 606 | const parent = property.model!; |
| 607 | let key = program.checker!.getTypeName(parent); |
| 608 | let isQualifiedParamName = false; |
| 609 | |
| 610 | if (parent.properties.size > 1) { |
| 611 | key += `.${property.name}`; |
| 612 | isQualifiedParamName = true; |
| 613 | } |
| 614 | |
| 615 | // Try to shorten the type name to exclude the top-level service namespace |
| 616 | let baseKey = getRefSafeName(key); |
| 617 | if (serviceNamespace && key.startsWith(serviceNamespace)) { |
| 618 | baseKey = key.substring(serviceNamespace.length + 1); |
| 619 | |
| 620 | // If no parameter exists with the shortened name, use it, otherwise use the fully-qualified name |
| 621 | if (!root.components.parameters[baseKey] || isQualifiedParamName) { |
| 622 | key = baseKey; |
| 623 | } |
| 624 | } |
| 625 | |
| 626 | return key; |
| 627 | } |
| 628 | |
| 629 | function getSchemaForType(type: Type) { |
| 630 | const builtinType = mapCadlTypeToOpenAPI(type); |
| 631 | if (builtinType !== undefined) return builtinType; |
| 632 | |
| 633 | if (type.kind === "Array") { |
| 634 | return getSchemaForArray(type); |
| 635 | } else if (type.kind === "Model") { |
| 636 | return getSchemaForModel(type); |
| 637 | } else if (type.kind === "Union") { |
| 638 | return getSchemaForUnion(type); |
| 639 | } else if (type.kind === "UnionVariant") { |
| 640 | return getSchemaForUnionVariant(type); |
| 641 | } else if (type.kind === "Enum") { |
| 642 | return getSchemaForEnum(type); |
| 643 | } |
| 644 | |
| 645 | reportDiagnostic(program, { |
| 646 | code: "invalid-schema", |
| 647 | format: { type: type.kind }, |
| 648 | target: type, |
| 649 | }); |
| 650 | return undefined; |
| 651 | } |
| 652 | |
| 653 | function getSchemaForEnum(e: EnumType) { |
| 654 | const values = []; |
| 655 | if (e.members.length == 0) { |
| 656 | reportUnsupportedUnion("empty"); |
| 657 | return undefined; |
| 658 | } |
| 659 | const type = enumMemberType(e.members[0]); |
| 660 | for (const option of e.members) { |
| 661 | if (type !== enumMemberType(option)) { |
| 662 | reportUnsupportedUnion(); |
| 663 | continue; |
| 664 | } |
| 665 | |
| 666 | values.push(option.value ?? option.name); |
| 667 | } |
| 668 | |
| 669 | const schema: any = { type, description: getDoc(program, e) }; |
| 670 | if (values.length > 0) { |
| 671 | schema.enum = values; |
| 672 | } |
| 673 | |
| 674 | return schema; |
| 675 | function enumMemberType(member: EnumMemberType) { |
| 676 | if (typeof member.value === "number") { |
| 677 | return "number"; |
| 678 | } |
| 679 | return "string"; |
| 680 | } |
| 681 | |
| 682 | function reportUnsupportedUnion(messageId: "default" | "empty" = "default") { |
| 683 | reportDiagnostic(program, { code: "union-unsupported", messageId, target: e }); |
| 684 | } |
| 685 | } |
| 686 | |
| 687 | function getSchemaForUnion(union: UnionType) { |
| 688 | let type: string; |
| 689 | const nonNullOptions = union.options.filter((t) => !isNullType(t)); |
| 690 | const nullable = union.options.length != nonNullOptions.length; |
| 691 | if (nonNullOptions.length === 0) { |
| 692 | reportDiagnostic(program, { code: "union-null", target: union }); |
| 693 | return {}; |
| 694 | } |
| 695 | |
| 696 | const kind = nonNullOptions[0].kind; |
| 697 | switch (kind) { |
| 698 | case "String": |
| 699 | type = "string"; |
| 700 | break; |
| 701 | case "Number": |
| 702 | type = "number"; |
| 703 | break; |
| 704 | case "Boolean": |
| 705 | type = "boolean"; |
| 706 | break; |
| 707 | case "Model": |
| 708 | type = "model"; |
| 709 | break; |
| 710 | case "UnionVariant": |
| 711 | type = "model"; |
| 712 | break; |
| 713 | case "Array": |
| 714 | type = "array"; |
| 715 | break; |
| 716 | default: |
| 717 | reportUnsupportedUnionType(nonNullOptions[0]); |
| 718 | return {}; |
| 719 | } |
| 720 | |
| 721 | if (type === "model" || type === "array") { |
| 722 | if (nonNullOptions.length === 1) { |
| 723 | // Get the schema for the model type |
| 724 | const schema: any = getSchemaForType(nonNullOptions[0]); |
| 725 | if (nullable) { |
| 726 | schema["nullable"] = true; |
| 727 | } |
| 728 | |
| 729 | return schema; |
| 730 | } else { |
| 731 | const ofType = getOneOf(program, union) ? "oneOf" : "anyOf"; |
| 732 | const schema: any = { [ofType]: nonNullOptions.map((s) => getSchemaOrRef(s)) }; |
| 733 | return schema; |
| 734 | } |
| 735 | } |
| 736 | |
| 737 | const values = []; |
| 738 | for (const option of nonNullOptions) { |
| 739 | if (option.kind != kind) { |
| 740 | reportUnsupportedUnion(); |
| 741 | } |
| 742 | |
| 743 | // We already know it's not a model type |
| 744 | values.push((option as any).value); |
| 745 | } |
| 746 | |
| 747 | const schema: any = { type }; |
| 748 | if (values.length > 0) { |
| 749 | schema.enum = values; |
| 750 | } |
| 751 | if (nullable) { |
| 752 | schema["nullable"] = true; |
| 753 | } |
| 754 | |
| 755 | return schema; |
| 756 | |
| 757 | function reportUnsupportedUnionType(type: Type) { |
| 758 | reportDiagnostic(program, { |
| 759 | code: "union-unsupported", |
| 760 | messageId: "type", |
| 761 | format: { kind: type.kind }, |
| 762 | target: type, |
| 763 | }); |
| 764 | } |
| 765 | |
| 766 | function reportUnsupportedUnion() { |
| 767 | reportDiagnostic(program, { code: "union-unsupported", target: union }); |
| 768 | } |
| 769 | } |
| 770 | |
| 771 | function getSchemaForUnionVariant(variant: UnionTypeVariant) { |
| 772 | const schema: any = getSchemaForType(variant.type); |
| 773 | return schema; |
| 774 | } |
| 775 | |
| 776 | function getSchemaForArray(array: ArrayType) { |
| 777 | const target = array.elementType; |
| 778 | |
| 779 | return { |
| 780 | type: "array", |
| 781 | items: getSchemaOrRef(target), |
| 782 | }; |
| 783 | } |
| 784 | |
| 785 | function isNullType(type: Type): boolean { |
| 786 | return isIntrinsic(program, type) && getIntrinsicModelName(program, type) === "null"; |
| 787 | } |
| 788 | |
| 789 | function getDefaultValue(type: Type): any { |
| 790 | switch (type.kind) { |
| 791 | case "String": |
| 792 | return type.value; |
| 793 | case "Number": |
| 794 | return type.value; |
| 795 | case "Boolean": |
| 796 | return type.value; |
| 797 | case "Tuple": |
| 798 | return type.values.map(getDefaultValue); |
| 799 | default: |
| 800 | reportDiagnostic(program, { |
| 801 | code: "invalid-default", |
| 802 | format: { type: type.kind }, |
| 803 | target: type, |
| 804 | }); |
| 805 | } |
| 806 | } |
| 807 | |
| 808 | function includeDerivedModel(model: ModelType): boolean { |
| 809 | return ( |
| 810 | !isTemplate(model) && |
| 811 | (model.templateArguments === undefined || |
| 812 | model.templateArguments?.length === 0 || |
| 813 | model.derivedModels.length > 0) |
| 814 | ); |
| 815 | } |
| 816 | |
| 817 | function getSchemaForModel(model: ModelType) { |
| 818 | let modelSchema: OpenAPI3Schema & Required<Pick<OpenAPI3Schema, "properties">> = { |
| 819 | type: "object", |
| 820 | properties: {}, |
| 821 | description: getDoc(program, model), |
| 822 | }; |
| 823 | |
| 824 | const derivedModels = model.derivedModels.filter(includeDerivedModel); |
| 825 | // getSchemaOrRef on all children to push them into components.schemas |
| 826 | for (const child of derivedModels) { |
| 827 | getSchemaOrRef(child); |
| 828 | } |
| 829 | |
| 830 | const discriminator = getDiscriminator(program, model); |
| 831 | if (discriminator) { |
| 832 | if (!validateDiscriminator(discriminator, derivedModels)) { |
| 833 | // appropriate diagnostic is generated with the validate function |
| 834 | return {}; |
| 835 | } |
| 836 | |
| 837 | const openApiDiscriminator: OpenAPI3Discriminator = { ...discriminator }; |
| 838 | const mapping = getDiscriminatorMapping(discriminator, derivedModels); |
| 839 | if (mapping) { |
| 840 | openApiDiscriminator.mapping = mapping; |
| 841 | } |
| 842 | |
| 843 | modelSchema.discriminator = openApiDiscriminator; |
| 844 | modelSchema.properties[discriminator.propertyName] = { |
| 845 | type: "string", |
| 846 | description: `Discriminator property for ${model.name}.`, |
| 847 | }; |
| 848 | } |
| 849 | |
| 850 | applyExternalDocs(model, modelSchema); |
| 851 | |
| 852 | for (const [name, prop] of model.properties) { |
| 853 | if (!isSchemaProperty(prop)) { |
| 854 | continue; |
| 855 | } |
| 856 | |
| 857 | const description = getDoc(program, prop); |
| 858 | if (!prop.optional) { |
| 859 | if (!modelSchema.required) { |
| 860 | modelSchema.required = []; |
| 861 | } |
| 862 | modelSchema.required.push(name); |
| 863 | } |
| 864 | |
| 865 | // Apply decorators on the property to the type's schema |
| 866 | modelSchema.properties[name] = applyIntrinsicDecorators(prop, getSchemaOrRef(prop.type)); |
| 867 | if (description) { |
| 868 | modelSchema.properties[name].description = description; |
| 869 | } |
| 870 | |
| 871 | if (prop.default) { |
| 872 | modelSchema.properties[name].default = getDefaultValue(prop.default); |
| 873 | } |
| 874 | |
| 875 | // Should the property be marked as readOnly? |
| 876 | const vis = getVisibility(program, prop); |
| 877 | if (vis && vis.includes("read") && vis.length == 1) { |
| 878 | modelSchema.properties[name].readOnly = true; |
| 879 | } |
| 880 | |
| 881 | // Attach any additional OpenAPI extensions |
| 882 | attachExtensions(program, prop, modelSchema.properties[name]); |
| 883 | } |
| 884 | |
| 885 | // Special case: if a model type extends a single *templated* base type and |
| 886 | // has no properties of its own, absorb the definition of the base model |
| 887 | // into this schema definition. The assumption here is that any model type |
| 888 | // defined like this is just meant to rename the underlying instance of a |
| 889 | // templated type. |
| 890 | if ( |
| 891 | model.baseModel && |
| 892 | model.baseModel.templateArguments && |
| 893 | model.baseModel.templateArguments.length > 0 && |
| 894 | Object.keys(modelSchema.properties).length === 0 |
| 895 | ) { |
| 896 | // Take the base model schema but carry across the documentation property |
| 897 | // that we set before |
| 898 | const baseSchema = getSchemaForType(model.baseModel); |
| 899 | modelSchema = { |
| 900 | ...baseSchema, |
| 901 | description: modelSchema.description, |
| 902 | }; |
| 903 | } else if (model.baseModel) { |
| 904 | modelSchema.allOf = [getSchemaOrRef(model.baseModel)]; |
| 905 | } |
| 906 | |
| 907 | // Attach any OpenAPI extensions |
| 908 | attachExtensions(program, model, modelSchema); |
| 909 | return modelSchema; |
| 910 | } |
| 911 | |
| 912 | function attachExtensions(program: Program, type: Type, emitObject: any) { |
| 913 | // Attach any OpenAPI extensions |
| 914 | const extensions = getExtensions(program, type); |
| 915 | if (extensions) { |
| 916 | for (const key of extensions.keys()) { |
| 917 | emitObject[key] = extensions.get(key); |
| 918 | } |
| 919 | } |
| 920 | } |
| 921 | |
| 922 | function validateDiscriminator( |
| 923 | discriminator: Discriminator, |
| 924 | childModels: readonly ModelType[] |
| 925 | ): boolean { |
| 926 | const { propertyName } = discriminator; |
| 927 | const retVals = childModels.map((t) => { |
| 928 | const prop = getProperty(t, propertyName); |
| 929 | if (!prop) { |
| 930 | reportDiagnostic(program, { code: "discriminator", messageId: "missing", target: t }); |
| 931 | return false; |
| 932 | } |
| 933 | let retval = true; |
| 934 | if (!isOasString(prop.type)) { |
| 935 | reportDiagnostic(program, { code: "discriminator", messageId: "type", target: prop }); |
| 936 | retval = false; |
| 937 | } |
| 938 | if (prop.optional) { |
| 939 | reportDiagnostic(program, { code: "discriminator", messageId: "required", target: prop }); |
| 940 | retval = false; |
| 941 | } |
| 942 | return retval; |
| 943 | }); |
| 944 | // Map of discriminator value to the model in which it is declared |
| 945 | const discriminatorValues = new Map<string, string>(); |
| 946 | for (const t of childModels) { |
| 947 | // Get the discriminator property directly in the child model |
| 948 | const prop = t.properties?.get(propertyName); |
| 949 | // Issue warning diagnostic if discriminator property missing or is not a string literal |
| 950 | if (!prop || !isStringLiteral(prop.type)) { |
| 951 | reportDiagnostic(program, { |
| 952 | code: "discriminator-value", |
| 953 | messageId: "literal", |
| 954 | target: prop || t, |
| 955 | }); |
| 956 | } |
| 957 | if (prop) { |
| 958 | const vals = getStringValues(prop.type); |
| 959 | vals.forEach((val) => { |
| 960 | if (discriminatorValues.has(val)) { |
| 961 | reportDiagnostic(program, { |
| 962 | code: "discriminator", |
| 963 | messageId: "duplicate", |
| 964 | format: { val: val, model1: discriminatorValues.get(val)!, model2: t.name }, |
| 965 | target: prop, |
| 966 | }); |
| 967 | retVals.push(false); |
| 968 | } else { |
| 969 | discriminatorValues.set(val, t.name); |
| 970 | } |
| 971 | }); |
| 972 | } |
| 973 | } |
| 974 | return retVals.every((v) => v); |
| 975 | } |
| 976 | |
| 977 | function getDiscriminatorMapping( |
| 978 | discriminator: any, |
| 979 | derivedModels: readonly ModelType[] |
| 980 | ): Record<string, string> | undefined { |
| 981 | const { propertyName } = discriminator; |
| 982 | const getMapping = (t: ModelType): any => { |
| 983 | const prop = t.properties?.get(propertyName); |
| 984 | if (prop) { |
| 985 | return getStringValues(prop.type).flatMap((v) => [{ [v]: getSchemaOrRef(t).$ref }]); |
| 986 | } |
| 987 | return undefined; |
| 988 | }; |
| 989 | const mappings = derivedModels.flatMap(getMapping).filter((v) => v); // only defined values |
| 990 | return mappings.length > 0 ? mappings.reduce((a, s) => ({ ...a, ...s }), {}) : undefined; |
| 991 | } |
| 992 | |
| 993 | // An openapi "string" can be defined in several different ways in Cadl |
| 994 | function isOasString(type: Type): boolean { |
| 995 | if (type.kind === "String") { |
| 996 | // A string literal |
| 997 | return true; |
| 998 | } else if (type.kind === "Model" && type.name === "string") { |
| 999 | // string type |
| 1000 | return true; |
| 1001 | } else if (type.kind === "Union") { |
| 1002 | // A union where all variants are an OasString |
| 1003 | return type.options.every((o) => isOasString(o)); |
| 1004 | } |
| 1005 | return false; |
| 1006 | } |
| 1007 | |
| 1008 | function isStringLiteral(type: Type): boolean { |
| 1009 | return ( |
| 1010 | type.kind === "String" || |
| 1011 | (type.kind === "Union" && type.options.every((o) => o.kind === "String")) |
| 1012 | ); |
| 1013 | } |
| 1014 | |
| 1015 | // Return any string literal values for type |
| 1016 | function getStringValues(type: Type): string[] { |
| 1017 | if (type.kind === "String") { |
| 1018 | return [type.value]; |
| 1019 | } else if (type.kind === "Union") { |
| 1020 | return type.options.flatMap(getStringValues).filter((v) => v); |
| 1021 | } |
| 1022 | return []; |
| 1023 | } |
| 1024 | |
| 1025 | /** |
| 1026 | * A "schema property" here is a property that is emitted to OpenAPI schema. |
| 1027 | * |
| 1028 | * Headers, parameters, status codes are not schema properties even they are |
| 1029 | * represented as properties in Cadl. |
| 1030 | */ |
| 1031 | function isSchemaProperty(property: ModelTypeProperty) { |
| 1032 | const headerInfo = getHeaderFieldName(program, property); |
| 1033 | const queryInfo = getQueryParamName(program, property); |
| 1034 | const pathInfo = getPathParamName(program, property); |
| 1035 | const statusCodeinfo = isStatusCode(program, property); |
| 1036 | return !(headerInfo || queryInfo || pathInfo || statusCodeinfo); |
| 1037 | } |
| 1038 | |
| 1039 | function getTypeNameForSchemaProperties(type: Type) { |
| 1040 | // If there's a friendly name for the type, use that instead |
| 1041 | let typeName = getFriendlyName(program, type); |
| 1042 | if (typeName) { |
| 1043 | return typeName; |
| 1044 | } |
| 1045 | |
| 1046 | // Try to shorten the type name to exclude the top-level service namespace |
| 1047 | typeName = program!.checker!.getTypeName(type).replace(/<([\w.]+)>/, "_$1"); |
| 1048 | |
| 1049 | if (isRefSafeName(typeName)) { |
| 1050 | if (serviceNamespace) { |
| 1051 | typeName = typeName.replace(RegExp(serviceNamespace + "\\.", "g"), ""); |
| 1052 | } |
| 1053 | // exclude the Cadl namespace in type names |
| 1054 | typeName = typeName.replace(/($|_)(Cadl\.)/g, "$1"); |
| 1055 | } |
| 1056 | |
| 1057 | return typeName; |
| 1058 | } |
| 1059 | |
| 1060 | function applyIntrinsicDecorators(cadlType: ModelType | ModelTypeProperty, target: any): any { |
| 1061 | const newTarget = { ...target }; |
| 1062 | |
| 1063 | const docStr = getDoc(program, cadlType); |
| 1064 | const isString = isStringType(program, getPropertyType(cadlType)); |
| 1065 | const isNumeric = isNumericType(program, getPropertyType(cadlType)); |
| 1066 | |
| 1067 | if (isString && !target.documentation && docStr) { |
| 1068 | newTarget.description = docStr; |
| 1069 | } |
| 1070 | |
| 1071 | const summaryStr = getSummary(program, cadlType); |
| 1072 | if (isString && !target.summary && summaryStr) { |
| 1073 | newTarget.summary = summaryStr; |
| 1074 | } |
| 1075 | |
| 1076 | const formatStr = getFormat(program, cadlType); |
| 1077 | if (isString && !target.format && formatStr) { |
| 1078 | newTarget.format = formatStr; |
| 1079 | } |
| 1080 | |
| 1081 | const pattern = getPattern(program, cadlType); |
| 1082 | if (isString && !target.pattern && pattern) { |
| 1083 | newTarget.pattern = pattern; |
| 1084 | } |
| 1085 | |
| 1086 | const minLength = getMinLength(program, cadlType); |
| 1087 | if (isString && !target.minLength && minLength !== undefined) { |
| 1088 | newTarget.minLength = minLength; |
| 1089 | } |
| 1090 | |
| 1091 | const maxLength = getMaxLength(program, cadlType); |
| 1092 | if (isString && !target.maxLength && maxLength !== undefined) { |
| 1093 | newTarget.maxLength = maxLength; |
| 1094 | } |
| 1095 | |
| 1096 | const minValue = getMinValue(program, cadlType); |
| 1097 | if (isNumeric && !target.minimum && minValue !== undefined) { |
| 1098 | newTarget.minimum = minValue; |
| 1099 | } |
| 1100 | |
| 1101 | const maxValue = getMaxValue(program, cadlType); |
| 1102 | if (isNumeric && !target.maximum && maxValue !== undefined) { |
| 1103 | newTarget.maximum = maxValue; |
| 1104 | } |
| 1105 | |
| 1106 | if (isSecret(program, cadlType)) { |
| 1107 | newTarget.format = "password"; |
| 1108 | } |
| 1109 | |
| 1110 | if (isString) { |
| 1111 | const values = getKnownValues(program, cadlType); |
| 1112 | if (values) { |
| 1113 | return { |
| 1114 | oneOf: [newTarget, getSchemaForEnum(values)], |
| 1115 | }; |
| 1116 | } |
| 1117 | } |
| 1118 | |
| 1119 | return newTarget; |
| 1120 | } |
| 1121 | |
| 1122 | function applyExternalDocs(cadlType: Type, target: Record<string, unknown>) { |
| 1123 | const externalDocs = getExternalDocs(program, cadlType); |
| 1124 | if (externalDocs) { |
| 1125 | target.externalDocs = externalDocs; |
| 1126 | } |
| 1127 | } |
| 1128 | |
| 1129 | // Map an Cadl type to an OA schema. Returns undefined when the resulting |
| 1130 | // OA schema is just a regular object schema. |
| 1131 | function mapCadlTypeToOpenAPI(cadlType: Type): any { |
| 1132 | switch (cadlType.kind) { |
| 1133 | case "Number": |
| 1134 | return { type: "number", enum: [cadlType.value] }; |
| 1135 | case "String": |
| 1136 | return { type: "string", enum: [cadlType.value] }; |
| 1137 | case "Boolean": |
| 1138 | return { type: "boolean", enum: [cadlType.value] }; |
| 1139 | case "Model": |
| 1140 | return mapCadlIntrinsicModelToOpenAPI(cadlType); |
| 1141 | } |
| 1142 | // // The base model doesn't correspond to a primitive OA type, but it could |
| 1143 | // // derive from one. Let's check. |
| 1144 | // if (cadlType.kind === "Model" && cadlType.baseModel) { |
| 1145 | // const baseSchema = mapCadlTypeToOpenAPI(cadlType.baseModel); |
| 1146 | // if (baseSchema) { |
| 1147 | // return applyIntrinsicDecorators(cadlType, baseSchema); |
| 1148 | // } |
| 1149 | // } |
| 1150 | } |
| 1151 | |
| 1152 | /** |
| 1153 | * Map Cadl intrinsic models to open api definitions |
| 1154 | */ |
| 1155 | function mapCadlIntrinsicModelToOpenAPI(cadlType: ModelType): any | undefined { |
| 1156 | if (!isIntrinsic(program, cadlType)) { |
| 1157 | return undefined; |
| 1158 | } |
| 1159 | const name = getIntrinsicModelName(program, cadlType); |
| 1160 | switch (name) { |
| 1161 | case "bytes": |
| 1162 | return { type: "string", format: "byte" }; |
| 1163 | case "int8": |
| 1164 | return applyIntrinsicDecorators(cadlType, { type: "integer", format: "int8" }); |
| 1165 | case "int16": |
| 1166 | return applyIntrinsicDecorators(cadlType, { type: "integer", format: "int16" }); |
| 1167 | case "int32": |
| 1168 | return applyIntrinsicDecorators(cadlType, { type: "integer", format: "int32" }); |
| 1169 | case "int64": |
| 1170 | return applyIntrinsicDecorators(cadlType, { type: "integer", format: "int64" }); |
| 1171 | case "safeint": |
| 1172 | return applyIntrinsicDecorators(cadlType, { type: "integer", format: "int64" }); |
| 1173 | case "uint8": |
| 1174 | return applyIntrinsicDecorators(cadlType, { type: "integer", format: "uint8" }); |
| 1175 | case "uint16": |
| 1176 | return applyIntrinsicDecorators(cadlType, { type: "integer", format: "uint16" }); |
| 1177 | case "uint32": |
| 1178 | return applyIntrinsicDecorators(cadlType, { type: "integer", format: "uint32" }); |
| 1179 | case "uint64": |
| 1180 | return applyIntrinsicDecorators(cadlType, { type: "integer", format: "uint64" }); |
| 1181 | case "float64": |
| 1182 | return applyIntrinsicDecorators(cadlType, { type: "number", format: "double" }); |
| 1183 | case "float32": |
| 1184 | return applyIntrinsicDecorators(cadlType, { type: "number", format: "float" }); |
| 1185 | case "string": |
| 1186 | return applyIntrinsicDecorators(cadlType, { type: "string" }); |
| 1187 | case "boolean": |
| 1188 | return { type: "boolean" }; |
| 1189 | case "plainDate": |
| 1190 | return { type: "string", format: "date" }; |
| 1191 | case "zonedDateTime": |
| 1192 | return { type: "string", format: "date-time" }; |
| 1193 | case "plainTime": |
| 1194 | return { type: "string", format: "time" }; |
| 1195 | case "duration": |
| 1196 | return { type: "string", format: "duration" }; |
| 1197 | case "Map": |
| 1198 | // We assert on valType because Map types always have a type |
| 1199 | const valType = cadlType.properties.get("v"); |
| 1200 | return { |
| 1201 | type: "object", |
| 1202 | additionalProperties: getSchemaOrRef(valType!.type), |
| 1203 | }; |
| 1204 | } |
| 1205 | } |
| 1206 | } |
| 1207 | |
| 1208 | function isRefSafeName(name: string) { |
| 1209 | return /^[A-Za-z0-9-_.]+$/.test(name); |
| 1210 | } |
| 1211 | |
| 1212 | function getRefSafeName(name: string) { |
| 1213 | return name.replace(/^[A-Za-z0-9-_.]/g, "_"); |
| 1214 | } |
| 1215 | |
| 1216 | function prettierOutput(output: string) { |
| 1217 | return output + "\n"; |
| 1218 | } |
| 1219 | |
| 1220 | class ErrorTypeFoundError extends Error { |
| 1221 | constructor() { |
| 1222 | super("Error type found in evaluated Cadl output"); |
| 1223 | } |
| 1224 | } |
| 1225 | |