microsoft/typespec

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e966efd30e552f4e84d5ae7224515199738ddd8e

Branches

Tags

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

Clone

HTTPS

Download ZIP

packages/openapi3/src/openapi.ts

1224lines · modecode

1import {
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";
43import { getExtensions, getExternalDocs, getOperationId } from "@cadl-lang/openapi";
44import {
45 Discriminator,
46 getAllRoutes,
47 getContentTypes,
48 getDiscriminator,
49 http,
50 HttpOperationParameter,
51 HttpOperationParameters,
52 HttpOperationResponse,
53 OperationDetails,
54} from "@cadl-lang/rest";
55import { getVersionRecords } from "@cadl-lang/versioning";
56import { getOneOf, getRef } from "./decorators.js";
57import { OpenAPILibrary, reportDiagnostic } from "./lib.js";
58import {
59 OpenAPI3Discriminator,
60 OpenAPI3Operation,
61 OpenAPI3Parameter,
62 OpenAPI3ParameterType,
63 OpenAPI3Schema,
64} from "./types.js";
65
66const {
67 getHeaderFieldName,
68 getPathParamName,
69 getQueryParamName,
70 isStatusCode,
71 getStatusCodeDescription,
72} = http;
73
74export 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
88const securityDetailsKey = Symbol("securityDetails");
89interface SecurityDetails {
90 definitions: any;
91 requirements: any[];
92}
93
94function 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
105function getSecurityRequirements(program: Program, serviceNamespace: NamespaceType) {
106 return getSecurityDetails(program, serviceNamespace).requirements;
107}
108
109function getSecurityDefinitions(program: Program, serviceNamespace: NamespaceType) {
110 return getSecurityDetails(program, serviceNamespace).definitions;
111}
112
113export 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
132export 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
150export interface OpenAPIEmitterOptions {
151 outputFile: string;
152}
153
154function 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
1208function isRefSafeName(name: string) {
1209 return /^[A-Za-z0-9-_.]+$/.test(name);
1210}
1211
1212function getRefSafeName(name: string) {
1213 return name.replace(/^[A-Za-z0-9-_.]/g, "_");
1214}
1215
1216function prettierOutput(output: string) {
1217 return output + "\n";
1218}
1219
1220class ErrorTypeFoundError extends Error {
1221 constructor() {
1222 super("Error type found in evaluated Cadl output");
1223 }
1224}
1225