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