microsoft/typespec

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
bd60b0a68bb23817a01ab4aef22170c18abd81ed

Branches

Tags

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

Clone

HTTPS

Download ZIP

packages/openapi3/src/openapi.ts

1511lines · modecode

1import {
2 BooleanLiteral,
3 compilerAssert,
4 DiscriminatedUnion,
5 EmitContext,
6 emitFile,
7 Enum,
8 EnumMember,
9 getAllTags,
10 getAnyExtensionFromPath,
11 getDiscriminatedUnion,
12 getDiscriminator,
13 getDoc,
14 getFormat,
15 getKnownValues,
16 getMaxItems,
17 getMaxLength,
18 getMaxValue,
19 getMaxValueExclusive,
20 getMinItems,
21 getMinLength,
22 getMinValue,
23 getMinValueExclusive,
24 getNamespaceFullName,
25 getPattern,
26 getPropertyType,
27 getService,
28 getSummary,
29 ignoreDiagnostics,
30 IntrinsicScalarName,
31 IntrinsicType,
32 isDeprecated,
33 isErrorType,
34 isGlobalNamespace,
35 isNeverType,
36 isNullType,
37 isNumericType,
38 isSecret,
39 isStringType,
40 isTemplateDeclaration,
41 isTemplateDeclarationOrInstance,
42 listServices,
43 Model,
44 ModelProperty,
45 Namespace,
46 navigateTypesInNamespace,
47 NewLine,
48 NumericLiteral,
49 Program,
50 ProjectionApplication,
51 projectProgram,
52 resolvePath,
53 Scalar,
54 Service,
55 StringLiteral,
56 TwoLevelMap,
57 Type,
58 TypeNameOptions,
59 Union,
60 UnionVariant,
61} from "@cadl-lang/compiler";
62
63import {
64 checkDuplicateTypeName,
65 getExtensions,
66 getExternalDocs,
67 getOpenAPITypeName,
68 getParameterKey,
69 isReadonlyProperty,
70 resolveOperationId,
71 shouldInline,
72} from "@cadl-lang/openapi";
73import { http } from "@cadl-lang/rest";
74import {
75 createMetadataInfo,
76 getAuthentication,
77 getHttpService,
78 getRequestVisibility,
79 getStatusCodeDescription,
80 getVisibilitySuffix,
81 HttpAuth,
82 HttpOperation,
83 HttpOperationParameter,
84 HttpOperationParameters,
85 HttpOperationResponse,
86 isContentTypeHeader,
87 isOverloadSameEndpoint,
88 MetadataInfo,
89 reportIfNoRoutes,
90 ServiceAuthentication,
91 Visibility,
92} from "@cadl-lang/rest/http";
93import { buildVersionProjections } from "@cadl-lang/versioning";
94import yaml from "js-yaml";
95import { getOneOf, getRef } from "./decorators.js";
96import { FileType, OpenAPI3EmitterOptions, reportDiagnostic } from "./lib.js";
97import {
98 OpenAPI3Discriminator,
99 OpenAPI3Document,
100 OpenAPI3Header,
101 OpenAPI3OAuthFlows,
102 OpenAPI3Operation,
103 OpenAPI3Parameter,
104 OpenAPI3ParameterBase,
105 OpenAPI3Schema,
106 OpenAPI3SchemaProperty,
107 OpenAPI3SecurityScheme,
108 OpenAPI3Server,
109 OpenAPI3ServerVariable,
110} from "./types.js";
111
112const defaultFileType: FileType = "yaml";
113const defaultOptions = {
114 "new-line": "lf",
115 "omit-unreachable-types": false,
116} as const;
117
118export async function $onEmit(context: EmitContext<OpenAPI3EmitterOptions>) {
119 const options = resolveOptions(context);
120 const emitter = createOAPIEmitter(context.program, options);
121 await emitter.emitOpenAPI();
122}
123
124function findFileTypeFromFilename(filename: string | undefined): FileType {
125 if (filename === undefined) {
126 return defaultFileType;
127 }
128 switch (getAnyExtensionFromPath(filename)) {
129 case ".yaml":
130 case ".yml":
131 return "yaml";
132 case ".json":
133 return "json";
134 default:
135 return defaultFileType;
136 }
137}
138export function resolveOptions(
139 context: EmitContext<OpenAPI3EmitterOptions>
140): ResolvedOpenAPI3EmitterOptions {
141 const resolvedOptions = { ...defaultOptions, ...context.options };
142
143 const fileType =
144 resolvedOptions["file-type"] ?? findFileTypeFromFilename(resolvedOptions["output-file"]);
145
146 const outputFile = resolvedOptions["output-file"] ?? `openapi.${fileType}`;
147 return {
148 fileType,
149 newLine: resolvedOptions["new-line"],
150 omitUnreachableTypes: resolvedOptions["omit-unreachable-types"],
151 outputFile: resolvePath(context.emitterOutputDir, outputFile),
152 };
153}
154
155export interface ResolvedOpenAPI3EmitterOptions {
156 fileType: FileType;
157 outputFile: string;
158 newLine: NewLine;
159 omitUnreachableTypes: boolean;
160}
161
162/**
163 * Represents a node that will hold a JSON reference. The value is computed
164 * at the end so that we can defer decisions about the name that is
165 * referenced.
166 */
167class Ref {
168 value?: string;
169 toJSON() {
170 compilerAssert(this.value, "Reference value never set.");
171 return this.value;
172 }
173}
174
175/**
176 * Represents a non-inlined schema that will be emitted as a definition.
177 * Computation of the OpenAPI schema object is deferred.
178 */
179interface PendingSchema {
180 /** The CADL type for the schema */
181 type: Type;
182
183 /** The visibility to apply when computing the schema */
184 visibility: Visibility;
185
186 /**
187 * The JSON reference to use to point to this schema.
188 *
189 * Note that its value will not be computed until all schemas have been
190 * computed as we will add a suffix to the name if more than one schema
191 * must be emitted for the type for different visibilities.
192 */
193 ref: Ref;
194}
195
196/**
197 * Represents a schema that is ready to emit as its OpenAPI representation
198 * has been produced.
199 */
200interface ProcessedSchema extends PendingSchema {
201 schema: OpenAPI3Schema | undefined;
202}
203
204function createOAPIEmitter(program: Program, options: ResolvedOpenAPI3EmitterOptions) {
205 let root: OpenAPI3Document;
206
207 // Get the service namespace string for use in name shortening
208 let serviceNamespace: string | undefined;
209 let currentPath: any;
210 let currentEndpoint: OpenAPI3Operation;
211
212 let metadataInfo: MetadataInfo;
213
214 // Keep a map of all Types+Visibility combinations that were encountered
215 // that need schema definitions.
216 let pendingSchemas = new TwoLevelMap<Type, Visibility, PendingSchema>();
217
218 // Reuse a single ref object per Type+Visibility combination.
219 let refs = new TwoLevelMap<Type, Visibility, Ref>();
220
221 // Keep track of inline types still in the process of having their schema computed
222 // This is used to detect cycles in inline types, which is an
223 let inProgressInlineTypes = new Set<Type>();
224
225 // Map model properties that represent shared parameters to their parameter
226 // definition that will go in #/components/parameters. Inlined parameters do not go in
227 // this map.
228 let params: Map<ModelProperty, any>;
229
230 // De-dupe the per-endpoint tags that will be added into the #/tags
231 let tags: Set<string>;
232
233 const typeNameOptions: TypeNameOptions = {
234 // shorten type names by removing Cadl and service namespace
235 namespaceFilter(ns) {
236 const name = getNamespaceFullName(ns);
237 return name !== "Cadl" && name !== serviceNamespace;
238 },
239 };
240
241 return { emitOpenAPI };
242
243 function initializeEmitter(service: Service, version?: string) {
244 const auth = processAuth(service.type);
245
246 root = {
247 openapi: "3.0.0",
248 info: {
249 title: service.title ?? "(title)",
250 version: version ?? service.version ?? "0000-00-00",
251 description: getDoc(program, service.type),
252 },
253 externalDocs: getExternalDocs(program, service.type),
254 tags: [],
255 paths: {},
256 security: auth?.security,
257 components: {
258 parameters: {},
259 requestBodies: {},
260 responses: {},
261 schemas: {},
262 examples: {},
263 securitySchemes: auth?.securitySchemes ?? {},
264 },
265 };
266 const servers = http.getServers(program, service.type);
267 if (servers) {
268 root.servers = resolveServers(servers);
269 }
270
271 serviceNamespace = getNamespaceFullName(service.type);
272 currentPath = root.paths;
273 pendingSchemas = new TwoLevelMap();
274 refs = new TwoLevelMap();
275 metadataInfo = createMetadataInfo(program, {
276 canShareProperty: (p) => isReadonlyProperty(program, p),
277 });
278 inProgressInlineTypes = new Set();
279 params = new Map();
280 tags = new Set();
281 }
282
283 function isValidServerVariableType(program: Program, type: Type): boolean {
284 switch (type.kind) {
285 case "String":
286 case "Union":
287 case "Scalar":
288 return ignoreDiagnostics(
289 program.checker.isTypeAssignableTo(
290 type.projectionBase ?? type,
291 program.checker.getStdType("string"),
292 type
293 )
294 );
295 case "Enum":
296 for (const member of type.members.values()) {
297 if (member.value && typeof member.value !== "string") {
298 return false;
299 }
300 }
301 return true;
302 default:
303 return false;
304 }
305 }
306
307 function validateValidServerVariable(program: Program, prop: ModelProperty) {
308 const isValid = isValidServerVariableType(program, prop.type);
309
310 if (!isValid) {
311 reportDiagnostic(program, {
312 code: "invalid-server-variable",
313 format: { propName: prop.name },
314 target: prop,
315 });
316 }
317 return isValid;
318 }
319
320 function resolveServers(servers: http.HttpServer[]): OpenAPI3Server[] {
321 return servers.map((server) => {
322 const variables: Record<string, OpenAPI3ServerVariable> = {};
323 for (const [name, prop] of server.parameters) {
324 if (!validateValidServerVariable(program, prop)) {
325 continue;
326 }
327
328 const variable: OpenAPI3ServerVariable = {
329 default: prop.default ? getDefaultValue(prop.default) : "",
330 description: getDoc(program, prop),
331 };
332
333 if (prop.type.kind === "Enum") {
334 variable.enum = getSchemaForEnum(prop.type).enum;
335 } else if (prop.type.kind === "Union") {
336 variable.enum = getSchemaForUnion(prop.type, Visibility.All).enum;
337 } else if (prop.type.kind === "String") {
338 variable.enum = [prop.type.value];
339 }
340 attachExtensions(program, prop, variable);
341 variables[name] = variable;
342 }
343 return {
344 url: server.url,
345 description: server.description,
346 variables,
347 };
348 });
349 }
350
351 async function emitOpenAPI() {
352 const services = listServices(program);
353 if (services.length === 0) {
354 services.push({ type: program.getGlobalNamespaceType() });
355 }
356 for (const service of services) {
357 const commonProjections: ProjectionApplication[] = [
358 {
359 projectionName: "target",
360 arguments: ["json"],
361 },
362 ];
363 const originalProgram = program;
364 const versions = buildVersionProjections(program, service.type);
365 for (const record of versions) {
366 const projectedProgram = (program = projectProgram(originalProgram, [
367 ...commonProjections,
368 ...record.projections,
369 ]));
370 const projectedServiceNs: Namespace = projectedProgram.projector.projectedTypes.get(
371 service.type
372 ) as Namespace;
373
374 await emitOpenAPIFromVersion(
375 projectedServiceNs === projectedProgram.getGlobalNamespaceType()
376 ? { type: projectedProgram.getGlobalNamespaceType() }
377 : getService(program, projectedServiceNs)!,
378 services.length > 1,
379 record.version
380 );
381 }
382 }
383 }
384
385 function resolveOutputFile(service: Service, multipleService: boolean, version?: string): string {
386 const suffix = [];
387 if (multipleService) {
388 suffix.push(getNamespaceFullName(service.type));
389 }
390 if (version) {
391 suffix.push(version);
392 }
393 if (suffix.length === 0) {
394 return options.outputFile;
395 }
396
397 const extension = getAnyExtensionFromPath(options.outputFile);
398 const filenameWithoutExtension = options.outputFile.slice(0, -extension.length);
399 return `${filenameWithoutExtension}.${suffix.join(".")}${extension}`;
400 }
401
402 async function emitOpenAPIFromVersion(
403 service: Service,
404 multipleService: boolean,
405 version?: string
406 ) {
407 initializeEmitter(service, version);
408 try {
409 const httpService = ignoreDiagnostics(getHttpService(program, service.type));
410 reportIfNoRoutes(program, httpService.operations);
411
412 for (const operation of httpService.operations) {
413 if (operation.overloading !== undefined && isOverloadSameEndpoint(operation as any)) {
414 continue;
415 } else {
416 emitOperation(operation);
417 }
418 }
419 emitParameters();
420 emitUnreferencedSchemas(service.type);
421 emitSchemas();
422 emitTags();
423
424 // Clean up empty entries
425 if (root.components) {
426 for (const elem of Object.keys(root.components)) {
427 if (Object.keys(root.components[elem as any]).length === 0) {
428 delete root.components[elem as any];
429 }
430 }
431 }
432
433 if (!program.compilerOptions.noEmit && !program.hasError()) {
434 // Write out the OpenAPI document to the output path
435
436 await emitFile(program, {
437 path: resolveOutputFile(service, multipleService, version),
438 content: serializeDocument(root, options.fileType),
439 newLine: options.newLine,
440 });
441 }
442 } catch (err) {
443 if (err instanceof ErrorTypeFoundError) {
444 // Return early, there must be a parse error if an ErrorType was
445 // inserted into the Cadl output
446 return;
447 } else {
448 throw err;
449 }
450 }
451 }
452
453 function emitOperation(operation: HttpOperation): void {
454 const { path: fullPath, operation: op, verb, parameters } = operation;
455
456 // If path contains a query string, issue msg and don't emit this endpoint
457 if (fullPath.indexOf("?") > 0) {
458 reportDiagnostic(program, { code: "path-query", target: op });
459 return;
460 }
461
462 if (!root.paths[fullPath]) {
463 root.paths[fullPath] = {};
464 }
465
466 currentPath = root.paths[fullPath];
467 if (!currentPath[verb]) {
468 currentPath[verb] = {};
469 }
470 currentEndpoint = currentPath[verb];
471
472 const currentTags = getAllTags(program, op);
473 if (currentTags) {
474 currentEndpoint.tags = currentTags;
475 for (const tag of currentTags) {
476 // Add to root tags if not already there
477 tags.add(tag);
478 }
479 }
480
481 currentEndpoint.operationId = resolveOperationId(program, op);
482 applyExternalDocs(op, currentEndpoint);
483
484 // Set up basic endpoint fields
485 currentEndpoint.summary = getSummary(program, op);
486 currentEndpoint.description = getDoc(program, op);
487 currentEndpoint.parameters = [];
488 currentEndpoint.responses = {};
489
490 const visibility = getRequestVisibility(verb);
491 emitEndpointParameters(parameters.parameters, visibility);
492 emitRequestBody(parameters, visibility);
493 emitResponses(operation.responses);
494
495 if (isDeprecated(program, op)) {
496 currentEndpoint.deprecated = true;
497 }
498
499 attachExtensions(program, op, currentEndpoint);
500 }
501
502 function emitResponses(responses: HttpOperationResponse[]) {
503 for (const response of responses) {
504 emitResponseObject(response);
505 }
506 }
507
508 function isBinaryPayload(body: Type, contentType: string) {
509 return (
510 body.kind === "Scalar" &&
511 body.name === "bytes" &&
512 contentType !== "application/json" &&
513 contentType !== "text/plain"
514 );
515 }
516
517 function getOpenAPIStatuscode(response: HttpOperationResponse): string {
518 switch (response.statusCode) {
519 case "*":
520 return "default";
521 default:
522 return response.statusCode;
523 }
524 }
525
526 function emitResponseObject(response: Readonly<HttpOperationResponse>) {
527 const statusCode = getOpenAPIStatuscode(response);
528 const openapiResponse = currentEndpoint.responses[statusCode] ?? {
529 description: response.description ?? getResponseDescriptionForStatusCode(statusCode),
530 };
531
532 for (const data of response.responses) {
533 if (data.headers && Object.keys(data.headers).length > 0) {
534 openapiResponse.headers ??= {};
535 // OpenAPI can't represent different headers per content type.
536 // So we merge headers here, and report any duplicates.
537 // It may be possible in principle to not error for identically declared
538 // headers.
539 for (const [key, value] of Object.entries(data.headers)) {
540 if (openapiResponse.headers[key]) {
541 reportDiagnostic(program, {
542 code: "duplicate-header",
543 format: { header: key },
544 target: response.type,
545 });
546 continue;
547 }
548 openapiResponse.headers[key] = getResponseHeader(value);
549 }
550 }
551
552 if (data.body !== undefined) {
553 openapiResponse.content ??= {};
554 for (const contentType of data.body.contentTypes) {
555 const isBinary = isBinaryPayload(data.body.type, contentType);
556 const schema = isBinary
557 ? { type: "string", format: "binary" }
558 : getSchemaOrRef(data.body.type, Visibility.Read);
559 openapiResponse.content[contentType] = { schema };
560 }
561 }
562 }
563
564 currentEndpoint.responses[statusCode] = openapiResponse;
565 }
566
567 function getResponseDescriptionForStatusCode(statusCode: string) {
568 if (statusCode === "default") {
569 return "An unexpected error response.";
570 }
571 return getStatusCodeDescription(statusCode) ?? "unknown";
572 }
573
574 function getResponseHeader(prop: ModelProperty): OpenAPI3Header | undefined {
575 return getOpenAPIParameterBase(prop, Visibility.Read);
576 }
577
578 function getSchemaOrRef(type: Type, visibility: Visibility): any {
579 const refUrl = getRef(program, type);
580 if (refUrl) {
581 return {
582 $ref: refUrl,
583 };
584 }
585
586 if (type.kind === "Scalar" && program.checker.isStdType(type)) {
587 return getSchemaForScalar(type);
588 }
589
590 if (type.kind === "String" || type.kind === "Number" || type.kind === "Boolean") {
591 // For literal types, we just want to emit them directly as well.
592 return mapCadlTypeToOpenAPI(type, visibility);
593 }
594
595 if (type.kind === "Intrinsic" && type.name === "unknown") {
596 return getSchemaForIntrinsicType(type);
597 }
598
599 if (type.kind === "EnumMember") {
600 // Enum members are just the OA representation of their values.
601 if (typeof type.value === "number") {
602 return { type: "number", enum: [type.value] };
603 } else {
604 return { type: "string", enum: [type.value ?? type.name] };
605 }
606 }
607
608 if (type.kind === "ModelProperty") {
609 return resolveProperty(type, visibility);
610 }
611
612 type = metadataInfo.getEffectivePayloadType(type, visibility);
613
614 const name = getOpenAPITypeName(program, type, typeNameOptions);
615 if (shouldInline(program, type)) {
616 const schema = getSchemaForInlineType(type, visibility, name);
617
618 if (schema === undefined && isErrorType(type)) {
619 // Exit early so that syntax errors are exposed. This error will
620 // be caught and handled in emitOpenAPI.
621 throw new ErrorTypeFoundError();
622 }
623
624 // helps to read output and correlate to Cadl
625 if (schema) {
626 schema["x-cadl-name"] = name;
627 }
628 return schema;
629 } else {
630 // Use shared schema when type is not transformed by visibility.
631 if (!metadataInfo.isTransformed(type, visibility)) {
632 visibility = Visibility.All;
633 }
634 const pending = pendingSchemas.getOrAdd(type, visibility, () => ({
635 type,
636 visibility,
637 ref: refs.getOrAdd(type, visibility, () => new Ref()),
638 }));
639 return { $ref: pending.ref };
640 }
641 }
642
643 function getSchemaForInlineType(type: Type, visibility: Visibility, name: string) {
644 if (inProgressInlineTypes.has(type)) {
645 reportDiagnostic(program, {
646 code: "inline-cycle",
647 format: { type: name },
648 target: type,
649 });
650 return {};
651 }
652 inProgressInlineTypes.add(type);
653 const schema = getSchemaForType(type, visibility);
654 inProgressInlineTypes.delete(type);
655 return schema;
656 }
657
658 function getParamPlaceholder(property: ModelProperty) {
659 let spreadParam = false;
660
661 if (property.sourceProperty) {
662 // chase our sources all the way back to the first place this property
663 // was defined.
664 spreadParam = true;
665 property = property.sourceProperty;
666 while (property.sourceProperty) {
667 property = property.sourceProperty;
668 }
669 }
670
671 const refUrl = getRef(program, property);
672 if (refUrl) {
673 return {
674 $ref: refUrl,
675 };
676 }
677
678 if (params.has(property)) {
679 return params.get(property);
680 }
681
682 const placeholder = {};
683
684 // only parameters inherited by spreading from non-inlined type are shared in #/components/parameters
685 if (spreadParam && property.model && !shouldInline(program, property.model)) {
686 params.set(property, placeholder);
687 }
688
689 return placeholder;
690 }
691
692 function emitEndpointParameters(parameters: HttpOperationParameter[], visibility: Visibility) {
693 for (const httpOpParam of parameters) {
694 if (params.has(httpOpParam.param)) {
695 currentEndpoint.parameters.push(params.get(httpOpParam.param));
696 continue;
697 }
698 if (httpOpParam.type === "header" && isContentTypeHeader(program, httpOpParam.param)) {
699 continue;
700 }
701 emitParameter(httpOpParam, visibility);
702 }
703 }
704
705 function emitRequestBody(parameters: HttpOperationParameters, visibility: Visibility) {
706 const body = parameters.body;
707 if (body === undefined) {
708 return;
709 }
710
711 const requestBody: any = {
712 description: body.parameter ? getDoc(program, body.parameter) : undefined,
713 content: {},
714 };
715
716 const contentTypes = body.contentTypes.length > 0 ? body.contentTypes : ["application/json"];
717 for (const contentType of contentTypes) {
718 const isBinary = isBinaryPayload(body.type, contentType);
719 const bodySchema = isBinary
720 ? { type: "string", format: "binary" }
721 : getSchemaOrRef(body.type, visibility);
722 const contentEntry: any = {
723 schema: bodySchema,
724 };
725 requestBody.content[contentType] = contentEntry;
726 }
727
728 currentEndpoint.requestBody = requestBody;
729 }
730
731 function emitParameter(parameter: HttpOperationParameter, visibility: Visibility) {
732 if (isNeverType(parameter.param.type)) {
733 return;
734 }
735 const ph = getParamPlaceholder(parameter.param);
736 currentEndpoint.parameters.push(ph);
737
738 // If the parameter already has a $ref, don't bother populating it
739 if (!("$ref" in ph)) {
740 populateParameter(ph, parameter, visibility);
741 }
742 }
743
744 function getOpenAPIParameterBase(
745 param: ModelProperty,
746 visibility: Visibility
747 ): OpenAPI3ParameterBase | undefined {
748 const typeSchema = getSchemaForType(param.type, visibility);
749 if (!typeSchema) {
750 return undefined;
751 }
752 const schema = applyIntrinsicDecorators(param, typeSchema);
753 if (param.default) {
754 schema.default = getDefaultValue(param.default);
755 }
756 // Description is already provided in the parameter itself.
757 delete schema.description;
758
759 const oaiParam: OpenAPI3ParameterBase = {
760 required: !param.optional,
761 description: getDoc(program, param),
762 schema,
763 };
764
765 attachExtensions(program, param, oaiParam);
766
767 return oaiParam;
768 }
769
770 function populateParameter(
771 ph: OpenAPI3Parameter,
772 parameter: HttpOperationParameter,
773 visibility: Visibility
774 ) {
775 ph.name = parameter.name;
776 ph.in = parameter.type;
777 if (parameter.type === "query") {
778 if (parameter.format === "csv") {
779 ph.style = "simple";
780 } else if (parameter.format === "multi") {
781 ph.style = "form";
782 ph.explode = true;
783 }
784 } else if (parameter.type === "header") {
785 if (parameter.format === "csv") {
786 ph.style = "simple";
787 }
788 }
789 Object.assign(ph, getOpenAPIParameterBase(parameter.param, visibility));
790 }
791
792 function emitParameters() {
793 for (const [property, param] of params) {
794 const key = getParameterKey(
795 program,
796 property,
797 param,
798 root.components!.parameters!,
799 typeNameOptions
800 );
801
802 root.components!.parameters![key] = { ...param };
803 for (const key of Object.keys(param)) {
804 delete param[key];
805 }
806
807 param.$ref = "#/components/parameters/" + encodeURIComponent(key);
808 }
809 }
810
811 function emitUnreferencedSchemas(namespace: Namespace) {
812 if (options.omitUnreachableTypes) {
813 return;
814 }
815 const computeSchema = (x: Type) => getSchemaOrRef(x, Visibility.All);
816
817 const skipSubNamespaces = isGlobalNamespace(program, namespace);
818 navigateTypesInNamespace(
819 namespace,
820 {
821 model: (x) => x.name !== "" && computeSchema(x),
822 scalar: computeSchema,
823 enum: computeSchema,
824 union: (x) => x.name !== undefined && computeSchema(x),
825 },
826 { skipSubNamespaces }
827 );
828 }
829
830 function emitSchemas() {
831 // Process pending schemas. Note that getSchemaForType may pull in new
832 // pending schemas so we iterate until there are no pending schemas
833 // remaining.
834 const processedSchemas = new TwoLevelMap<Type, Visibility, ProcessedSchema>();
835 while (pendingSchemas.size > 0) {
836 for (const [type, group] of pendingSchemas) {
837 for (const [visibility, pending] of group) {
838 processedSchemas.getOrAdd(type, visibility, () => ({
839 ...pending,
840 schema: getSchemaForType(type, visibility),
841 }));
842 }
843 pendingSchemas.delete(type);
844 }
845 }
846
847 // Emit the processed schemas. Only now can we compute the names as it
848 // depends on whether we have produced multiple schemas for a single
849 // CADL type.
850 for (const group of processedSchemas.values()) {
851 for (const [visibility, processed] of group) {
852 let name = getOpenAPITypeName(program, processed.type, typeNameOptions);
853 if (group.size > 1) {
854 name += getVisibilitySuffix(visibility);
855 }
856 checkDuplicateTypeName(program, processed.type, name, root.components!.schemas);
857 processed.ref.value = "#/components/schemas/" + encodeURIComponent(name);
858 if (processed.schema) {
859 root.components!.schemas![name] = processed.schema;
860 }
861 }
862 }
863 }
864
865 function emitTags() {
866 for (const tag of tags) {
867 root.tags!.push({ name: tag });
868 }
869 }
870
871 function getSchemaForType(type: Type, visibility: Visibility): OpenAPI3Schema | undefined {
872 const builtinType = mapCadlTypeToOpenAPI(type, visibility);
873 if (builtinType !== undefined) return builtinType;
874
875 switch (type.kind) {
876 case "Intrinsic":
877 return getSchemaForIntrinsicType(type);
878 case "Model":
879 return getSchemaForModel(type, visibility);
880 case "ModelProperty":
881 return getSchemaForType(type.type, visibility);
882 case "Scalar":
883 return getSchemaForScalar(type);
884 case "Union":
885 return getSchemaForUnion(type, visibility);
886 case "UnionVariant":
887 return getSchemaForUnionVariant(type, visibility);
888 case "Enum":
889 return getSchemaForEnum(type);
890 case "Tuple":
891 return { type: "array", items: {} };
892 case "TemplateParameter":
893 // Note: This should never happen if it does there is a bug in the compiler.
894 reportDiagnostic(program, {
895 code: "invalid-schema",
896 format: { type: `${type.node.id.sv} (template parameter)` },
897 target: type,
898 });
899 return undefined;
900 }
901
902 reportDiagnostic(program, {
903 code: "invalid-schema",
904 format: { type: type.kind },
905 target: type,
906 });
907 return undefined;
908 }
909
910 function getSchemaForIntrinsicType(type: IntrinsicType): OpenAPI3Schema {
911 switch (type.name) {
912 case "unknown":
913 return {};
914 }
915
916 reportDiagnostic(program, {
917 code: "invalid-schema",
918 format: { type: type.name },
919 target: type,
920 });
921 return {};
922 }
923
924 function getSchemaForEnum(e: Enum) {
925 const values = [];
926 if (e.members.size === 0) {
927 reportUnsupportedUnion("empty");
928 return undefined;
929 }
930 const type = enumMemberType(e.members.values().next().value);
931 for (const option of e.members.values()) {
932 if (type !== enumMemberType(option)) {
933 reportUnsupportedUnion();
934 continue;
935 }
936
937 values.push(option.value ?? option.name);
938 }
939
940 const schema: any = { type, description: getDoc(program, e) };
941 if (values.length > 0) {
942 schema.enum = values;
943 }
944
945 return schema;
946 function enumMemberType(member: EnumMember) {
947 if (typeof member.value === "number") {
948 return "number";
949 }
950 return "string";
951 }
952
953 function reportUnsupportedUnion(messageId: "default" | "empty" = "default") {
954 reportDiagnostic(program, { code: "union-unsupported", messageId, target: e });
955 }
956 }
957
958 /**
959 * A Cadl union maps to a variety of OA3 structures according to the following rules:
960 *
961 * * A union containing `null` makes a `nullable` schema comprised of the remaining
962 * union variants.
963 * * A union containing literal types are converted to OA3 enums. All literals of the
964 * same type are combined into single enums.
965 * * A union that contains multiple items (after removing null and combining like-typed
966 * literals into enums) is an `anyOf` union unless `oneOf` is applied to the union
967 * declaration.
968 */
969 function getSchemaForUnion(union: Union, visibility: Visibility) {
970 const variants = Array.from(union.variants.values());
971 const literalVariantEnumByType: Record<string, any> = {};
972 const ofType = getOneOf(program, union) ? "oneOf" : "anyOf";
973 const schemaMembers: { schema: any; type: Type | null }[] = [];
974 let nullable = false;
975 const discriminator = getDiscriminator(program, union);
976
977 for (const variant of variants) {
978 if (isNullType(variant.type)) {
979 nullable = true;
980 continue;
981 }
982
983 if (isLiteralType(variant.type)) {
984 if (!literalVariantEnumByType[variant.type.kind]) {
985 const enumSchema = mapCadlTypeToOpenAPI(variant.type, visibility);
986 literalVariantEnumByType[variant.type.kind] = enumSchema;
987 schemaMembers.push({ schema: enumSchema, type: null });
988 } else {
989 literalVariantEnumByType[variant.type.kind].enum.push(variant.type.value);
990 }
991 continue;
992 }
993
994 schemaMembers.push({ schema: getSchemaOrRef(variant.type, visibility), type: variant.type });
995 }
996
997 if (schemaMembers.length === 0) {
998 if (nullable) {
999 // This union is equivalent to just `null` but OA3 has no way to specify
1000 // null as a value, so we throw an error.
1001 reportDiagnostic(program, { code: "union-null", target: union });
1002 return {};
1003 } else {
1004 // completely empty union can maybe only happen with bugs?
1005 compilerAssert(false, "Attempting to emit an empty union");
1006 }
1007 }
1008
1009 if (schemaMembers.length === 1) {
1010 // we can just return the single schema member after applying nullable
1011 const schema = schemaMembers[0].schema;
1012 const type = schemaMembers[0].type;
1013
1014 if (nullable) {
1015 if (schema.$ref) {
1016 // but we can't make a ref "nullable", so wrap in an allOf (for models)
1017 // or oneOf (for all other types)
1018 if (type && type.kind === "Model") {
1019 return { type: "object", allOf: [schema], nullable: true };
1020 } else {
1021 return { oneOf: [schema], nullable: true };
1022 }
1023 } else {
1024 schema.nullable = true;
1025 }
1026 }
1027
1028 return schema;
1029 }
1030
1031 const schema: any = {
1032 [ofType]: schemaMembers.map((m) => m.schema),
1033 };
1034
1035 if (nullable) {
1036 schema.nullable = true;
1037 }
1038
1039 if (discriminator) {
1040 // the decorator validates that all the variants will be a model type
1041 // with the discriminator field present.
1042 schema.discriminator = discriminator;
1043 // Diagnostic already reported in compiler for unions
1044 const discriminatedUnion = ignoreDiagnostics(getDiscriminatedUnion(union, discriminator));
1045 if (discriminatedUnion.variants.size > 0) {
1046 schema.discriminator.mapping = getDiscriminatorMapping(discriminatedUnion, visibility);
1047 }
1048 }
1049
1050 return schema;
1051 }
1052
1053 function getSchemaForUnionVariant(variant: UnionVariant, visibility: Visibility) {
1054 const schema: any = getSchemaForType(variant.type, visibility);
1055 return schema;
1056 }
1057
1058 function isLiteralType(type: Type): type is StringLiteral | NumericLiteral | BooleanLiteral {
1059 return type.kind === "Boolean" || type.kind === "String" || type.kind === "Number";
1060 }
1061
1062 function getDefaultValue(type: Type): any {
1063 switch (type.kind) {
1064 case "String":
1065 return type.value;
1066 case "Number":
1067 return type.value;
1068 case "Boolean":
1069 return type.value;
1070 case "Tuple":
1071 return type.values.map(getDefaultValue);
1072 case "EnumMember":
1073 return type.value ?? type.name;
1074 default:
1075 reportDiagnostic(program, {
1076 code: "invalid-default",
1077 format: { type: type.kind },
1078 target: type,
1079 });
1080 }
1081 }
1082
1083 function includeDerivedModel(model: Model): boolean {
1084 return (
1085 !isTemplateDeclaration(model) &&
1086 (model.templateArguments === undefined ||
1087 model.templateArguments?.length === 0 ||
1088 model.derivedModels.length > 0)
1089 );
1090 }
1091
1092 function getSchemaForModel(model: Model, visibility: Visibility) {
1093 let modelSchema: OpenAPI3Schema & Required<Pick<OpenAPI3Schema, "properties">> = {
1094 type: "object",
1095 properties: {},
1096 description: getDoc(program, model),
1097 };
1098
1099 const derivedModels = model.derivedModels.filter(includeDerivedModel);
1100 // getSchemaOrRef on all children to push them into components.schemas
1101 for (const child of derivedModels) {
1102 getSchemaOrRef(child, visibility);
1103 }
1104
1105 const discriminator = getDiscriminator(program, model);
1106 if (discriminator) {
1107 const [union] = getDiscriminatedUnion(model, discriminator);
1108
1109 const openApiDiscriminator: OpenAPI3Discriminator = { ...discriminator };
1110 if (union.variants.size > 0) {
1111 openApiDiscriminator.mapping = getDiscriminatorMapping(union, visibility);
1112 }
1113
1114 modelSchema.discriminator = openApiDiscriminator;
1115 modelSchema.properties[discriminator.propertyName] = {
1116 type: "string",
1117 description: `Discriminator property for ${model.name}.`,
1118 };
1119 }
1120
1121 applyExternalDocs(model, modelSchema);
1122
1123 for (const [name, prop] of model.properties) {
1124 if (!metadataInfo.isPayloadProperty(prop, visibility)) {
1125 continue;
1126 }
1127
1128 if (isNeverType(prop.type)) {
1129 // If the property has a type of 'never', don't include it in the schema
1130 continue;
1131 }
1132
1133 if (!metadataInfo.isOptional(prop, visibility)) {
1134 if (!modelSchema.required) {
1135 modelSchema.required = [];
1136 }
1137 modelSchema.required.push(name);
1138 }
1139
1140 modelSchema.properties[name] = resolveProperty(prop, visibility);
1141 }
1142
1143 // Special case: if a model type extends a single *templated* base type and
1144 // has no properties of its own, absorb the definition of the base model
1145 // into this schema definition. The assumption here is that any model type
1146 // defined like this is just meant to rename the underlying instance of a
1147 // templated type.
1148 if (
1149 model.baseModel &&
1150 isTemplateDeclarationOrInstance(model.baseModel) &&
1151 Object.keys(modelSchema.properties).length === 0
1152 ) {
1153 // Take the base model schema but carry across the documentation property
1154 // that we set before
1155 const baseSchema = getSchemaForType(model.baseModel, visibility);
1156 modelSchema = {
1157 ...(baseSchema as any),
1158 description: modelSchema.description,
1159 };
1160 } else if (model.baseModel) {
1161 modelSchema.allOf = [getSchemaOrRef(model.baseModel, visibility)];
1162 }
1163
1164 // Attach any OpenAPI extensions
1165 attachExtensions(program, model, modelSchema);
1166 return modelSchema;
1167 }
1168
1169 function resolveProperty(prop: ModelProperty, visibility: Visibility): OpenAPI3SchemaProperty {
1170 const description = getDoc(program, prop);
1171
1172 const schema = getSchemaOrRef(prop.type, visibility);
1173 // Apply decorators on the property to the type's schema
1174 const additionalProps: Partial<OpenAPI3Schema> = applyIntrinsicDecorators(prop, {});
1175 if (description) {
1176 additionalProps.description = description;
1177 }
1178
1179 if (prop.default) {
1180 additionalProps.default = getDefaultValue(prop.default);
1181 }
1182
1183 if (isReadonlyProperty(program, prop)) {
1184 additionalProps.readOnly = true;
1185 }
1186
1187 // Attach any additional OpenAPI extensions
1188 attachExtensions(program, prop, additionalProps);
1189 if (schema && "$ref" in schema) {
1190 if (Object.keys(additionalProps).length === 0) {
1191 return schema;
1192 } else {
1193 return {
1194 allOf: [schema],
1195 ...additionalProps,
1196 };
1197 }
1198 } else {
1199 return { ...schema, ...additionalProps };
1200 }
1201 }
1202
1203 function attachExtensions(program: Program, type: Type, emitObject: any) {
1204 // Attach any OpenAPI extensions
1205 const extensions = getExtensions(program, type);
1206 if (extensions) {
1207 for (const key of extensions.keys()) {
1208 emitObject[key] = extensions.get(key);
1209 }
1210 }
1211 }
1212
1213 function getDiscriminatorMapping(union: DiscriminatedUnion, visibility: Visibility) {
1214 const mapping: Record<string, string> | undefined = {};
1215 for (const [key, model] of union.variants.entries()) {
1216 mapping[key] = getSchemaOrRef(model, visibility).$ref;
1217 }
1218 return mapping;
1219 }
1220
1221 function applyIntrinsicDecorators(
1222 cadlType: Scalar | ModelProperty,
1223 target: OpenAPI3Schema
1224 ): OpenAPI3Schema {
1225 const newTarget = { ...target };
1226 const docStr = getDoc(program, cadlType);
1227 const isString = isStringType(program, getPropertyType(cadlType));
1228 const isNumeric = isNumericType(program, getPropertyType(cadlType));
1229
1230 if (!target.description && docStr) {
1231 newTarget.description = docStr;
1232 }
1233 const formatStr = getFormat(program, cadlType);
1234 if (isString && !target.format && formatStr) {
1235 newTarget.format = formatStr;
1236 }
1237
1238 const pattern = getPattern(program, cadlType);
1239 if (isString && !target.pattern && pattern) {
1240 newTarget.pattern = pattern;
1241 }
1242
1243 const minLength = getMinLength(program, cadlType);
1244 if (isString && !target.minLength && minLength !== undefined) {
1245 newTarget.minLength = minLength;
1246 }
1247
1248 const maxLength = getMaxLength(program, cadlType);
1249 if (isString && !target.maxLength && maxLength !== undefined) {
1250 newTarget.maxLength = maxLength;
1251 }
1252
1253 const minValue = getMinValue(program, cadlType);
1254 if (isNumeric && !target.minimum && minValue !== undefined) {
1255 newTarget.minimum = minValue;
1256 }
1257
1258 const minValueExclusive = getMinValueExclusive(program, cadlType);
1259 if (isNumeric && !target.exclusiveMinimum && minValueExclusive !== undefined) {
1260 newTarget.exclusiveMinimum = minValueExclusive;
1261 }
1262
1263 const maxValue = getMaxValue(program, cadlType);
1264 if (isNumeric && !target.maximum && maxValue !== undefined) {
1265 newTarget.maximum = maxValue;
1266 }
1267
1268 const maxValueExclusive = getMaxValueExclusive(program, cadlType);
1269 if (isNumeric && !target.exclusiveMaximum && maxValueExclusive !== undefined) {
1270 newTarget.exclusiveMaximum = maxValueExclusive;
1271 }
1272
1273 const minItems = getMinItems(program, cadlType);
1274 if (!target.minItems && minItems !== undefined) {
1275 newTarget.minItems = minItems;
1276 }
1277
1278 const maxItems = getMaxItems(program, cadlType);
1279 if (!target.maxItems && maxItems !== undefined) {
1280 newTarget.maxItems = maxItems;
1281 }
1282
1283 if (isSecret(program, cadlType)) {
1284 newTarget.format = "password";
1285 }
1286
1287 if (isString) {
1288 const values = getKnownValues(program, cadlType);
1289 if (values) {
1290 return {
1291 oneOf: [newTarget, getSchemaForEnum(values)],
1292 };
1293 }
1294 }
1295
1296 attachExtensions(program, cadlType, newTarget);
1297
1298 return newTarget;
1299 }
1300
1301 function applyExternalDocs(cadlType: Type, target: Record<string, unknown>) {
1302 const externalDocs = getExternalDocs(program, cadlType);
1303 if (externalDocs) {
1304 target.externalDocs = externalDocs;
1305 }
1306 }
1307
1308 // Map an Cadl type to an OA schema. Returns undefined when the resulting
1309 // OA schema is just a regular object schema.
1310 function mapCadlTypeToOpenAPI(cadlType: Type, visibility: Visibility): any {
1311 switch (cadlType.kind) {
1312 case "Number":
1313 return { type: "number", enum: [cadlType.value] };
1314 case "String":
1315 return { type: "string", enum: [cadlType.value] };
1316 case "Boolean":
1317 return { type: "boolean", enum: [cadlType.value] };
1318 case "Model":
1319 return mapCadlIntrinsicModelToOpenAPI(cadlType, visibility);
1320 }
1321 }
1322
1323 /**
1324 * Map Cadl intrinsic models to open api definitions
1325 */
1326 function mapCadlIntrinsicModelToOpenAPI(
1327 cadlType: Model,
1328 visibility: Visibility
1329 ): any | undefined {
1330 if (cadlType.indexer) {
1331 if (isNeverType(cadlType.indexer.key)) {
1332 } else {
1333 const name = cadlType.indexer.key.name;
1334 if (name === "string") {
1335 return {
1336 type: "object",
1337 additionalProperties: getSchemaOrRef(cadlType.indexer.value!, visibility),
1338 };
1339 } else if (name === "integer") {
1340 return {
1341 type: "array",
1342 items: getSchemaOrRef(cadlType.indexer.value!, visibility | Visibility.Item),
1343 };
1344 }
1345 }
1346 }
1347 }
1348
1349 function getSchemaForScalar(scalar: Scalar): OpenAPI3Schema {
1350 let result: OpenAPI3Schema = {};
1351 if (program.checker.isStdType(scalar)) {
1352 result = getSchemaForStdScalars(scalar);
1353 } else if (scalar.baseScalar) {
1354 result = getSchemaForScalar(scalar.baseScalar);
1355 }
1356 return applyIntrinsicDecorators(scalar, result);
1357 }
1358
1359 function getSchemaForStdScalars(scalar: Scalar & { name: IntrinsicScalarName }): OpenAPI3Schema {
1360 switch (scalar.name) {
1361 case "bytes":
1362 return { type: "string", format: "byte" };
1363 case "int8":
1364 return { type: "integer", format: "int8" };
1365 case "int16":
1366 return { type: "integer", format: "int16" };
1367 case "int32":
1368 return { type: "integer", format: "int32" };
1369 case "int64":
1370 return { type: "integer", format: "int64" };
1371 case "safeint":
1372 return { type: "integer", format: "int64" };
1373 case "uint8":
1374 return { type: "integer", format: "uint8" };
1375 case "uint16":
1376 return { type: "integer", format: "uint16" };
1377 case "uint32":
1378 return { type: "integer", format: "uint32" };
1379 case "uint64":
1380 return { type: "integer", format: "uint64" };
1381 case "float64":
1382 return { type: "number", format: "double" };
1383 case "float32":
1384 return { type: "number", format: "float" };
1385 case "string":
1386 return { type: "string" };
1387 case "boolean":
1388 return { type: "boolean" };
1389 case "plainDate":
1390 return { type: "string", format: "date" };
1391 case "zonedDateTime":
1392 return { type: "string", format: "date-time" };
1393 case "plainTime":
1394 return { type: "string", format: "time" };
1395 case "duration":
1396 return { type: "string", format: "duration" };
1397 case "url":
1398 return { type: "string", format: "uri" };
1399 case "integer":
1400 case "numeric":
1401 case "float":
1402 return {}; // Waiting on design for more precise type https://github.com/microsoft/cadl/issues/1260
1403 default:
1404 const _assertNever: never = scalar.name;
1405 return {};
1406 }
1407 }
1408
1409 function processAuth(serviceNamespace: Namespace):
1410 | {
1411 securitySchemes: Record<string, OpenAPI3SecurityScheme>;
1412 security: Record<string, string[]>[];
1413 }
1414 | undefined {
1415 const authentication = getAuthentication(program, serviceNamespace);
1416 if (authentication) {
1417 return processServiceAuthentication(authentication);
1418 }
1419 return undefined;
1420 }
1421
1422 function processServiceAuthentication(authentication: ServiceAuthentication): {
1423 securitySchemes: Record<string, OpenAPI3SecurityScheme>;
1424 security: Record<string, string[]>[];
1425 } {
1426 const oaiSchemes: Record<string, OpenAPI3SecurityScheme> = {};
1427 const security: Record<string, string[]>[] = [];
1428 for (const option of authentication.options) {
1429 const oai3SecurityOption: Record<string, string[]> = {};
1430 for (const scheme of option.schemes) {
1431 const [oaiScheme, scopes] = getOpenAPI3Scheme(scheme);
1432 oaiSchemes[scheme.id] = oaiScheme;
1433 oai3SecurityOption[scheme.id] = scopes;
1434 }
1435 security.push(oai3SecurityOption);
1436 }
1437 return { securitySchemes: oaiSchemes, security };
1438 }
1439
1440 function getOpenAPI3Scheme(auth: HttpAuth): [OpenAPI3SecurityScheme, string[]] {
1441 switch (auth.type) {
1442 case "http":
1443 return [{ type: "http", scheme: auth.scheme, description: auth.description }, []];
1444 case "apiKey":
1445 return [
1446 { type: "apiKey", in: auth.in, name: auth.name, description: auth.description },
1447 [],
1448 ];
1449 case "oauth2":
1450 const flows: OpenAPI3OAuthFlows = {};
1451 const scopes: string[] = [];
1452 for (const flow of auth.flows) {
1453 scopes.push(...flow.scopes.map((x) => x.value));
1454 flows[flow.type] = {
1455 authorizationUrl: (flow as any).authorizationUrl,
1456 tokenUrl: (flow as any).tokenUrl,
1457 refreshUrl: flow.refreshUrl,
1458 scopes: Object.fromEntries(flow.scopes.map((x) => [x.value, x.description ?? ""])),
1459 };
1460 }
1461 return [{ type: "oauth2", flows, description: auth.description }, scopes];
1462 default:
1463 const _assertNever: never = auth;
1464 compilerAssert(false, "Unreachable");
1465 }
1466 }
1467}
1468
1469function serializeDocument(root: OpenAPI3Document, fileType: FileType): string {
1470 sortOpenAPIDocument(root);
1471 switch (fileType) {
1472 case "json":
1473 return prettierOutput(JSON.stringify(root, null, 2));
1474 case "yaml":
1475 return yaml.dump(root, {
1476 noRefs: true,
1477 replacer: function (key, value) {
1478 return value instanceof Ref ? value.toJSON() : value;
1479 },
1480 });
1481 }
1482}
1483
1484function prettierOutput(output: string) {
1485 return output + "\n";
1486}
1487
1488class ErrorTypeFoundError extends Error {
1489 constructor() {
1490 super("Error type found in evaluated Cadl output");
1491 }
1492}
1493
1494function sortObjectByKeys<T extends Record<string, unknown>>(obj: T): T {
1495 return Object.keys(obj)
1496 .sort()
1497 .reduce((sortedObj: any, key: string) => {
1498 sortedObj[key] = obj[key];
1499 return sortedObj;
1500 }, {});
1501}
1502
1503function sortOpenAPIDocument(doc: OpenAPI3Document): void {
1504 doc.paths = sortObjectByKeys(doc.paths);
1505 if (doc.components?.schemas) {
1506 doc.components.schemas = sortObjectByKeys(doc.components.schemas);
1507 }
1508 if (doc.components?.parameters) {
1509 doc.components.parameters = sortObjectByKeys(doc.components.parameters);
1510 }
1511}