microsoft/typespec

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
3fdd7fc5612f6f0db1446a76d6a915c10330db22

Branches

Tags

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

Clone

HTTPS

Download ZIP

packages/openapi3/src/openapi.ts

1179lines · modecode

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