@rexeus/typeweaver-openapi 0.11.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/README.md +97 -0
- package/dist/LICENSE +202 -0
- package/dist/NOTICE +4 -0
- package/dist/index.cjs +1317 -0
- package/dist/index.d.cts +130 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +130 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1294 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +68 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1294 @@
|
|
|
1
|
+
import { pascalCase } from "polycase";
|
|
2
|
+
import { fromZod } from "@rexeus/typeweaver-zod-to-json-schema";
|
|
3
|
+
import "zod";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { BasePlugin } from "@rexeus/typeweaver-gen";
|
|
6
|
+
//#region src/internal/zodIntrospection.ts
|
|
7
|
+
const TRANSPARENT_WRAPPER_TYPES = new Set([
|
|
8
|
+
"optional",
|
|
9
|
+
"nullable",
|
|
10
|
+
"default",
|
|
11
|
+
"catch",
|
|
12
|
+
"prefault",
|
|
13
|
+
"readonly",
|
|
14
|
+
"nonoptional"
|
|
15
|
+
]);
|
|
16
|
+
function getSchemaDefinition(schema) {
|
|
17
|
+
if (schema === void 0) return;
|
|
18
|
+
const schemaWithDefinition = schema;
|
|
19
|
+
return schemaWithDefinition._zod?.def ?? schemaWithDefinition.def ?? schemaWithDefinition._def;
|
|
20
|
+
}
|
|
21
|
+
function getSchemaType(schema) {
|
|
22
|
+
if (schema === void 0) return;
|
|
23
|
+
return getSchemaDefinition(schema)?.type ?? "unknown";
|
|
24
|
+
}
|
|
25
|
+
function isZodTransparentWrapperType(schemaType, transparentWrapperTypes = TRANSPARENT_WRAPPER_TYPES) {
|
|
26
|
+
return schemaType !== void 0 && transparentWrapperTypes.has(schemaType);
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/internal/bodyContent.ts
|
|
30
|
+
const OPEN_API_BINARY_SCHEMA = {
|
|
31
|
+
type: "string",
|
|
32
|
+
format: "binary"
|
|
33
|
+
};
|
|
34
|
+
const RAW_BODY_TRANSPARENT_WRAPPER_TYPES = new Set([
|
|
35
|
+
"optional",
|
|
36
|
+
"nullable",
|
|
37
|
+
"default",
|
|
38
|
+
"catch",
|
|
39
|
+
"prefault",
|
|
40
|
+
"readonly",
|
|
41
|
+
"nonoptional"
|
|
42
|
+
]);
|
|
43
|
+
function resolveOpenApiBodySchema(body, registerSchema) {
|
|
44
|
+
return shouldUseBinarySchema(body) ? {
|
|
45
|
+
schema: OPEN_API_BINARY_SCHEMA,
|
|
46
|
+
schemaKey: "openapi-binary",
|
|
47
|
+
warnings: []
|
|
48
|
+
} : registerSchema();
|
|
49
|
+
}
|
|
50
|
+
function shouldUseBinarySchema(body) {
|
|
51
|
+
return body.transport === "raw" && mediaTypeEssence(body.mediaType) === "application/octet-stream" && (body.mediaTypeSource === "raw-fallback" || isAmbiguousRawSchema(body.schema));
|
|
52
|
+
}
|
|
53
|
+
function mediaTypeEssence(mediaType) {
|
|
54
|
+
return mediaType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
55
|
+
}
|
|
56
|
+
function isAmbiguousRawSchema(schema) {
|
|
57
|
+
const schemaType = getSchemaType(unwrapTransparentSchema(schema));
|
|
58
|
+
return schemaType === "any" || schemaType === "unknown";
|
|
59
|
+
}
|
|
60
|
+
function unwrapTransparentSchema(schema) {
|
|
61
|
+
const visitedSchemas = /* @__PURE__ */ new Set();
|
|
62
|
+
let current = schema;
|
|
63
|
+
while (current !== void 0 && !visitedSchemas.has(current)) {
|
|
64
|
+
visitedSchemas.add(current);
|
|
65
|
+
const definition = getSchemaDefinition(current);
|
|
66
|
+
const schemaType = definition?.type;
|
|
67
|
+
if (isZodTransparentWrapperType(schemaType, RAW_BODY_TRANSPARENT_WRAPPER_TYPES)) {
|
|
68
|
+
current = definition?.innerType;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (schemaType === "pipe") {
|
|
72
|
+
const outputType = getSchemaType(definition?.out);
|
|
73
|
+
if (outputType === void 0 || outputType === "transform") return;
|
|
74
|
+
current = definition?.out;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (schemaType === "effects") {
|
|
78
|
+
current = definition?.schema;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
return current;
|
|
82
|
+
}
|
|
83
|
+
return current;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/internal/jsonPointer.ts
|
|
87
|
+
function jsonPointer(segments) {
|
|
88
|
+
if (segments.length === 0) return "";
|
|
89
|
+
return `/${segments.map(escapeJsonPointerSegment).join("/")}`;
|
|
90
|
+
}
|
|
91
|
+
function appendJsonPointer(base, suffix) {
|
|
92
|
+
if (suffix === "") return base;
|
|
93
|
+
if (base === "") return suffix;
|
|
94
|
+
return `${base}${suffix}`;
|
|
95
|
+
}
|
|
96
|
+
function isJsonPointerAtOrBelow(path, prefix) {
|
|
97
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
98
|
+
}
|
|
99
|
+
function escapeJsonPointerSegment(segment) {
|
|
100
|
+
return segment.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/internal/openApiPath.ts
|
|
104
|
+
function toOpenApiPath(path) {
|
|
105
|
+
return `/${normalizePathSegments(path).map((segment) => segment.replaceAll(pathParameterPattern, "{$1}")).join("/")}`;
|
|
106
|
+
}
|
|
107
|
+
function getPathParameterNames(path) {
|
|
108
|
+
return normalizePathSegments(path).flatMap((segment) => Array.from(segment.matchAll(pathParameterPattern), (match) => match[1] ?? ""));
|
|
109
|
+
}
|
|
110
|
+
const pathParameterPattern = /:([A-Za-z0-9_]+)/g;
|
|
111
|
+
function normalizePathSegments(path) {
|
|
112
|
+
return path.split("/").filter((segment) => segment.length > 0);
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/internal/operationContext.ts
|
|
116
|
+
function createOperationLocation(options) {
|
|
117
|
+
const isComponentResponseLocation = options.context.resourceName === "components.responses";
|
|
118
|
+
return {
|
|
119
|
+
resourceName: options.context.resourceName,
|
|
120
|
+
operationId: options.context.operation.operationId,
|
|
121
|
+
...isComponentResponseLocation ? {} : {
|
|
122
|
+
method: options.context.operation.method,
|
|
123
|
+
path: options.context.operation.path
|
|
124
|
+
},
|
|
125
|
+
openApiPath: options.context.openApiPath,
|
|
126
|
+
part: options.part,
|
|
127
|
+
parameterName: options.parameterName,
|
|
128
|
+
responseName: options.responseName,
|
|
129
|
+
statusCode: options.statusCode
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/internal/openApiSchemaNormalization.ts
|
|
134
|
+
const SCHEMA_CHILD_KEYS = new Set([
|
|
135
|
+
"items",
|
|
136
|
+
"additionalProperties",
|
|
137
|
+
"not",
|
|
138
|
+
"if",
|
|
139
|
+
"then",
|
|
140
|
+
"else",
|
|
141
|
+
"contains",
|
|
142
|
+
"propertyNames"
|
|
143
|
+
]);
|
|
144
|
+
const SCHEMA_CHILD_MAP_KEYS = new Set([
|
|
145
|
+
"properties",
|
|
146
|
+
"patternProperties",
|
|
147
|
+
"$defs",
|
|
148
|
+
"definitions",
|
|
149
|
+
"dependentSchemas"
|
|
150
|
+
]);
|
|
151
|
+
const SCHEMA_CHILD_ARRAY_KEYS = new Set([
|
|
152
|
+
"prefixItems",
|
|
153
|
+
"allOf",
|
|
154
|
+
"anyOf",
|
|
155
|
+
"oneOf"
|
|
156
|
+
]);
|
|
157
|
+
function normalizeOpenApiSchema(schema) {
|
|
158
|
+
const hasConst = hasOwnSchemaKeyword(schema, "const");
|
|
159
|
+
const normalizedEntries = Object.entries(schema).filter(([key]) => key !== "const").map(([key, value]) => [key, normalizeSchemaKeyword(key, value)]);
|
|
160
|
+
const constValue = schema.const;
|
|
161
|
+
if (hasConst && constValue !== void 0) normalizedEntries.push(["enum", [constValue]]);
|
|
162
|
+
return Object.fromEntries(normalizedEntries);
|
|
163
|
+
}
|
|
164
|
+
function normalizeSchemaKeyword(key, value) {
|
|
165
|
+
if (SCHEMA_CHILD_KEYS.has(key) && isJsonSchema$1(value)) return normalizeOpenApiSchema(value);
|
|
166
|
+
if (SCHEMA_CHILD_MAP_KEYS.has(key) && isJsonSchema$1(value)) return Object.fromEntries(Object.entries(value).map(([childKey, childValue]) => [childKey, isJsonSchema$1(childValue) ? normalizeOpenApiSchema(childValue) : childValue]));
|
|
167
|
+
if (SCHEMA_CHILD_ARRAY_KEYS.has(key) && Array.isArray(value)) return value.map((entry) => isJsonSchema$1(entry) ? normalizeOpenApiSchema(entry) : entry);
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
function isJsonSchema$1(value) {
|
|
171
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
172
|
+
}
|
|
173
|
+
function hasOwnSchemaKeyword(schema, keyword) {
|
|
174
|
+
return Object.prototype.hasOwnProperty.call(schema, keyword);
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/internal/schemaConversion.ts
|
|
178
|
+
function convertSchema(schema, documentPath, location, options = {}) {
|
|
179
|
+
const result = fromZod(schema);
|
|
180
|
+
const shouldRebaseLocalRefs = options.rebaseLocalRefs ?? true;
|
|
181
|
+
const openApiSchema = normalizeOpenApiSchema(result.schema);
|
|
182
|
+
return {
|
|
183
|
+
schema: shouldRebaseLocalRefs ? rebaseLocalJsonSchemaRefs(openApiSchema, documentPath) : openApiSchema,
|
|
184
|
+
warnings: result.warnings.map((warning) => ({
|
|
185
|
+
origin: "schema-conversion",
|
|
186
|
+
code: warning.code,
|
|
187
|
+
message: warning.message,
|
|
188
|
+
schemaType: warning.schemaType,
|
|
189
|
+
schemaPath: warning.path,
|
|
190
|
+
documentPath: appendJsonPointer(documentPath, warning.path),
|
|
191
|
+
location
|
|
192
|
+
}))
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function rebaseLocalJsonSchemaRefs(schema, documentPath) {
|
|
196
|
+
return rebaseJsonSchemaValue(schema, documentPath);
|
|
197
|
+
}
|
|
198
|
+
function unwrapRootOptional(schema) {
|
|
199
|
+
return {
|
|
200
|
+
schema: unwrapRootSchema(schema),
|
|
201
|
+
isOptional: omittedInputResult(schema) !== "rejects"
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function unwrapRootSchema(schema) {
|
|
205
|
+
const visitedSchemas = /* @__PURE__ */ new Set();
|
|
206
|
+
let current = schema;
|
|
207
|
+
while (!visitedSchemas.has(current)) {
|
|
208
|
+
visitedSchemas.add(current);
|
|
209
|
+
const definition = getSchemaDefinition(current);
|
|
210
|
+
const schemaType = definition?.type;
|
|
211
|
+
if (schemaType === "nullable") return current;
|
|
212
|
+
if (schemaType === "optional" || schemaType === "default" || schemaType === "catch" || schemaType === "prefault" || schemaType === "readonly" || schemaType === "nonoptional") {
|
|
213
|
+
const innerType = definition?.innerType;
|
|
214
|
+
if (innerType === void 0) return current;
|
|
215
|
+
current = innerType;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
return current;
|
|
219
|
+
}
|
|
220
|
+
return current;
|
|
221
|
+
}
|
|
222
|
+
function omittedInputResult(schema) {
|
|
223
|
+
const visitedSchemas = /* @__PURE__ */ new Set();
|
|
224
|
+
let current = schema;
|
|
225
|
+
while (!visitedSchemas.has(current)) {
|
|
226
|
+
visitedSchemas.add(current);
|
|
227
|
+
const definition = getSchemaDefinition(current);
|
|
228
|
+
const schemaType = definition?.type;
|
|
229
|
+
if (schemaType === "optional") return "accepts-undefined";
|
|
230
|
+
if (schemaType === "default" || schemaType === "prefault") return "accepts-defined";
|
|
231
|
+
if (schemaType === "catch") {
|
|
232
|
+
const innerType = definition?.innerType;
|
|
233
|
+
if (innerType === void 0) return "accepts-defined";
|
|
234
|
+
const innerResult = omittedInputResult(innerType);
|
|
235
|
+
return innerResult === "rejects" ? "accepts-defined" : innerResult;
|
|
236
|
+
}
|
|
237
|
+
if (schemaType === "nonoptional") {
|
|
238
|
+
const innerType = definition?.innerType;
|
|
239
|
+
if (innerType === void 0) return "rejects";
|
|
240
|
+
return omittedInputResult(innerType) === "accepts-defined" ? "accepts-defined" : "rejects";
|
|
241
|
+
}
|
|
242
|
+
if (schemaType === "nullable" || schemaType === "readonly") {
|
|
243
|
+
const innerType = definition?.innerType;
|
|
244
|
+
if (innerType === void 0) return "rejects";
|
|
245
|
+
current = innerType;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
return "rejects";
|
|
249
|
+
}
|
|
250
|
+
return "rejects";
|
|
251
|
+
}
|
|
252
|
+
function getObjectProperties(schema) {
|
|
253
|
+
const properties = schema.properties;
|
|
254
|
+
if (!isJsonSchema(properties)) return {};
|
|
255
|
+
return Object.fromEntries(Object.entries(properties).filter((entry) => isJsonSchema(entry[1])));
|
|
256
|
+
}
|
|
257
|
+
function preserveReferencedRootDefinitions(schema, rootSchema) {
|
|
258
|
+
return ["$defs", "definitions"].reduce((selfContainedSchema, definitionKey) => preserveReferencedRootDefinitionKeyword({
|
|
259
|
+
schema: selfContainedSchema,
|
|
260
|
+
sourceSchema: selfContainedSchema,
|
|
261
|
+
rootSchema,
|
|
262
|
+
definitionKey
|
|
263
|
+
}), schema);
|
|
264
|
+
}
|
|
265
|
+
function getRequiredNames(schema) {
|
|
266
|
+
if (!Array.isArray(schema.required)) return /* @__PURE__ */ new Set();
|
|
267
|
+
return new Set(schema.required.filter((entry) => typeof entry === "string"));
|
|
268
|
+
}
|
|
269
|
+
function hasUnrepresentableAdditionalProperties(schema) {
|
|
270
|
+
return Object.prototype.hasOwnProperty.call(schema, "additionalProperties") && schema.additionalProperties !== false;
|
|
271
|
+
}
|
|
272
|
+
function isJsonSchema(value) {
|
|
273
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
274
|
+
}
|
|
275
|
+
function rebaseJsonSchemaValue(value, documentPath) {
|
|
276
|
+
if (Array.isArray(value)) return value.map((item) => rebaseJsonSchemaValue(item, documentPath));
|
|
277
|
+
if (!isJsonSchema(value)) return value;
|
|
278
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, key === "$ref" && typeof child === "string" ? rebaseLocalJsonSchemaRef(child, documentPath) : rebaseJsonSchemaValue(child, documentPath)]));
|
|
279
|
+
}
|
|
280
|
+
function rebaseLocalJsonSchemaRef(ref, documentPath) {
|
|
281
|
+
if (ref === "#") return `#${documentPath}`;
|
|
282
|
+
if (ref.startsWith("#/")) return `#${appendJsonPointer(documentPath, ref.slice(1))}`;
|
|
283
|
+
return ref;
|
|
284
|
+
}
|
|
285
|
+
function preserveReferencedRootDefinitionKeyword(options) {
|
|
286
|
+
const rootDefinitions = options.rootSchema[options.definitionKey];
|
|
287
|
+
if (!isJsonSchema(rootDefinitions)) return options.schema;
|
|
288
|
+
const referencedDefinitions = collectReferencedRootDefinitions({
|
|
289
|
+
schema: options.sourceSchema,
|
|
290
|
+
rootDefinitions,
|
|
291
|
+
definitionKey: options.definitionKey
|
|
292
|
+
});
|
|
293
|
+
if (Object.keys(referencedDefinitions).length === 0) return options.schema;
|
|
294
|
+
const existingDefinitions = options.schema[options.definitionKey];
|
|
295
|
+
return {
|
|
296
|
+
...options.schema,
|
|
297
|
+
[options.definitionKey]: {
|
|
298
|
+
...isJsonSchema(existingDefinitions) ? existingDefinitions : {},
|
|
299
|
+
...referencedDefinitions
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function collectReferencedRootDefinitions(options) {
|
|
304
|
+
const pendingNames = [...collectReferencedDefinitionNames(options.schema, options.definitionKey)];
|
|
305
|
+
const copiedDefinitions = {};
|
|
306
|
+
for (const name of pendingNames) {
|
|
307
|
+
if (Object.prototype.hasOwnProperty.call(copiedDefinitions, name)) continue;
|
|
308
|
+
const definition = options.rootDefinitions[name];
|
|
309
|
+
if (definition === void 0) continue;
|
|
310
|
+
copiedDefinitions[name] = definition;
|
|
311
|
+
if (!isJsonSchema(definition)) continue;
|
|
312
|
+
for (const transitiveName of collectReferencedDefinitionNames(definition, options.definitionKey)) if (!Object.prototype.hasOwnProperty.call(copiedDefinitions, transitiveName)) pendingNames.push(transitiveName);
|
|
313
|
+
}
|
|
314
|
+
return copiedDefinitions;
|
|
315
|
+
}
|
|
316
|
+
function collectReferencedDefinitionNames(value, definitionKey) {
|
|
317
|
+
const names = /* @__PURE__ */ new Set();
|
|
318
|
+
collectReferencedDefinitionNamesFromValue(value, definitionKey, names);
|
|
319
|
+
return names;
|
|
320
|
+
}
|
|
321
|
+
function collectReferencedDefinitionNamesFromValue(value, definitionKey, names) {
|
|
322
|
+
if (Array.isArray(value)) {
|
|
323
|
+
value.forEach((item) => collectReferencedDefinitionNamesFromValue(item, definitionKey, names));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!isJsonSchema(value)) return;
|
|
327
|
+
Object.entries(value).forEach(([key, child]) => {
|
|
328
|
+
if (key === "$ref" && typeof child === "string") {
|
|
329
|
+
const definitionName = getReferencedRootDefinitionName(child, definitionKey);
|
|
330
|
+
if (definitionName !== void 0) names.add(definitionName);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
collectReferencedDefinitionNamesFromValue(child, definitionKey, names);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function getReferencedRootDefinitionName(ref, definitionKey) {
|
|
337
|
+
const prefix = `#/${escapeJsonPointerSegment(definitionKey)}/`;
|
|
338
|
+
if (!ref.startsWith(prefix)) return;
|
|
339
|
+
const [encodedName] = ref.slice(prefix.length).split("/");
|
|
340
|
+
if (encodedName === void 0 || encodedName === "") return;
|
|
341
|
+
return unescapeJsonPointerSegment(encodedName);
|
|
342
|
+
}
|
|
343
|
+
function unescapeJsonPointerSegment(segment) {
|
|
344
|
+
return segment.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/internal/parameterContainer.ts
|
|
348
|
+
function extractParameterContainer(options) {
|
|
349
|
+
if (options.schema === void 0) return {
|
|
350
|
+
properties: {},
|
|
351
|
+
requiredNames: /* @__PURE__ */ new Set(),
|
|
352
|
+
warnings: [],
|
|
353
|
+
isRootOptional: false
|
|
354
|
+
};
|
|
355
|
+
const optionalSchema = unwrapRootOptional(options.schema);
|
|
356
|
+
const converted = convertSchema(optionalSchema.schema, options.containerPointer, createOperationLocation({
|
|
357
|
+
context: options.context,
|
|
358
|
+
part: options.part,
|
|
359
|
+
responseName: options.responseName,
|
|
360
|
+
statusCode: options.statusCode
|
|
361
|
+
}), { rebaseLocalRefs: false });
|
|
362
|
+
const properties = Object.fromEntries(Object.entries(getObjectProperties(converted.schema)).map(([name, schema]) => [name, preserveReferencedRootDefinitions(schema, converted.schema)]));
|
|
363
|
+
const warnings = [...converted.warnings];
|
|
364
|
+
const hasFiniteProperties = Object.keys(properties).length > 0;
|
|
365
|
+
if (converted.schema.type !== "object") warnings.push(createContainerWarning({
|
|
366
|
+
code: "unrepresentable-parameter-container",
|
|
367
|
+
message: `${options.part} must be a finite object schema to become OpenAPI parameters.`,
|
|
368
|
+
documentPath: options.containerPointer,
|
|
369
|
+
context: options.context,
|
|
370
|
+
part: options.part,
|
|
371
|
+
responseName: options.responseName,
|
|
372
|
+
statusCode: options.statusCode
|
|
373
|
+
}));
|
|
374
|
+
else if (!hasFiniteProperties && hasUnrepresentableAdditionalProperties(converted.schema)) warnings.push(createContainerWarning({
|
|
375
|
+
code: "unrepresentable-parameter-container",
|
|
376
|
+
message: `${options.part} record entries cannot be represented as finite OpenAPI parameters.`,
|
|
377
|
+
documentPath: options.containerPointer,
|
|
378
|
+
context: options.context,
|
|
379
|
+
part: options.part,
|
|
380
|
+
responseName: options.responseName,
|
|
381
|
+
statusCode: options.statusCode
|
|
382
|
+
}));
|
|
383
|
+
else if (hasUnrepresentableAdditionalProperties(converted.schema)) warnings.push(createContainerWarning({
|
|
384
|
+
code: "unrepresentable-parameter-additional-properties",
|
|
385
|
+
message: `${options.part} additional properties cannot be represented as OpenAPI parameters.`,
|
|
386
|
+
documentPath: options.containerPointer,
|
|
387
|
+
context: options.context,
|
|
388
|
+
part: options.part,
|
|
389
|
+
responseName: options.responseName,
|
|
390
|
+
statusCode: options.statusCode
|
|
391
|
+
}));
|
|
392
|
+
return {
|
|
393
|
+
properties,
|
|
394
|
+
requiredNames: getRequiredNames(converted.schema),
|
|
395
|
+
warnings,
|
|
396
|
+
isRootOptional: optionalSchema.isOptional
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function createContainerWarning(options) {
|
|
400
|
+
return {
|
|
401
|
+
origin: "openapi-builder",
|
|
402
|
+
code: options.code,
|
|
403
|
+
message: options.message,
|
|
404
|
+
documentPath: options.documentPath,
|
|
405
|
+
location: createOperationLocation(options)
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region src/internal/parameters.ts
|
|
410
|
+
function buildRequestParameters(context) {
|
|
411
|
+
const operationPointer = jsonPointer([
|
|
412
|
+
"paths",
|
|
413
|
+
context.openApiPath,
|
|
414
|
+
context.method,
|
|
415
|
+
"parameters"
|
|
416
|
+
]);
|
|
417
|
+
const pathParameters = buildPathParameters(context, operationPointer);
|
|
418
|
+
const queryParameters = buildParametersFromContainer({
|
|
419
|
+
schema: context.operation.request?.query,
|
|
420
|
+
parameterIn: "query",
|
|
421
|
+
part: "request.query",
|
|
422
|
+
context,
|
|
423
|
+
startIndex: pathParameters.parameters.length,
|
|
424
|
+
parametersPointer: operationPointer
|
|
425
|
+
});
|
|
426
|
+
const headerParameters = buildParametersFromContainer({
|
|
427
|
+
schema: context.operation.request?.header,
|
|
428
|
+
parameterIn: "header",
|
|
429
|
+
part: "request.header",
|
|
430
|
+
context,
|
|
431
|
+
startIndex: pathParameters.parameters.length + queryParameters.parameters.length,
|
|
432
|
+
parametersPointer: operationPointer
|
|
433
|
+
});
|
|
434
|
+
return {
|
|
435
|
+
parameters: [
|
|
436
|
+
...pathParameters.parameters,
|
|
437
|
+
...queryParameters.parameters,
|
|
438
|
+
...headerParameters.parameters
|
|
439
|
+
],
|
|
440
|
+
warnings: [
|
|
441
|
+
...pathParameters.warnings,
|
|
442
|
+
...queryParameters.warnings,
|
|
443
|
+
...headerParameters.warnings
|
|
444
|
+
]
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function buildPathParameters(context, parametersPointer) {
|
|
448
|
+
const pathNames = getPathParameterNames(context.operation.path);
|
|
449
|
+
const container = extractParameterContainer({
|
|
450
|
+
schema: context.operation.request?.param,
|
|
451
|
+
context,
|
|
452
|
+
part: "request.path",
|
|
453
|
+
containerPointer: parametersPointer
|
|
454
|
+
});
|
|
455
|
+
const pathNameSet = new Set(pathNames);
|
|
456
|
+
const missingWarnings = [];
|
|
457
|
+
const unusedWarnings = Object.keys(container.properties).filter((name) => !pathNameSet.has(name)).map((name) => createParameterWarning({
|
|
458
|
+
code: "unused-path-parameter-schema",
|
|
459
|
+
message: `Path parameter schema '${name}' is not used by '${context.operation.path}'.`,
|
|
460
|
+
documentPath: parametersPointer,
|
|
461
|
+
context,
|
|
462
|
+
part: "request.path",
|
|
463
|
+
parameterName: name
|
|
464
|
+
}));
|
|
465
|
+
return {
|
|
466
|
+
parameters: pathNames.map((name, index) => {
|
|
467
|
+
const schema = container.properties[name];
|
|
468
|
+
const parameterPointer = jsonPointer([
|
|
469
|
+
"paths",
|
|
470
|
+
context.openApiPath,
|
|
471
|
+
context.method,
|
|
472
|
+
"parameters",
|
|
473
|
+
String(index)
|
|
474
|
+
]);
|
|
475
|
+
if (schema === void 0) missingWarnings.push(createParameterWarning({
|
|
476
|
+
code: "missing-path-parameter-schema",
|
|
477
|
+
message: `Path parameter '${name}' is missing a schema.`,
|
|
478
|
+
documentPath: `${parameterPointer}/schema`,
|
|
479
|
+
context,
|
|
480
|
+
part: "request.path",
|
|
481
|
+
parameterName: name
|
|
482
|
+
}));
|
|
483
|
+
return {
|
|
484
|
+
name,
|
|
485
|
+
in: "path",
|
|
486
|
+
required: true,
|
|
487
|
+
schema: schema === void 0 ? {} : rebaseLocalJsonSchemaRefs(schema, `${parameterPointer}/schema`)
|
|
488
|
+
};
|
|
489
|
+
}),
|
|
490
|
+
warnings: [
|
|
491
|
+
...rebaseParameterSchemaWarnings({
|
|
492
|
+
warnings: container.warnings,
|
|
493
|
+
context,
|
|
494
|
+
parameterNames: pathNames,
|
|
495
|
+
startIndex: 0
|
|
496
|
+
}),
|
|
497
|
+
...unusedWarnings,
|
|
498
|
+
...missingWarnings
|
|
499
|
+
]
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function buildParametersFromContainer(options) {
|
|
503
|
+
const container = extractParameterContainer({
|
|
504
|
+
schema: options.schema,
|
|
505
|
+
context: options.context,
|
|
506
|
+
part: options.part,
|
|
507
|
+
containerPointer: options.parametersPointer
|
|
508
|
+
});
|
|
509
|
+
return {
|
|
510
|
+
parameters: Object.entries(container.properties).map(([name, schema], index) => ({
|
|
511
|
+
name,
|
|
512
|
+
in: options.parameterIn,
|
|
513
|
+
required: !container.isRootOptional && container.requiredNames.has(name),
|
|
514
|
+
schema: rebaseLocalJsonSchemaRefs(schema, `${options.parametersPointer}/${options.startIndex + index}/schema`)
|
|
515
|
+
})),
|
|
516
|
+
warnings: rebaseParameterSchemaWarnings({
|
|
517
|
+
warnings: container.warnings,
|
|
518
|
+
context: options.context,
|
|
519
|
+
parameterNames: Object.keys(container.properties),
|
|
520
|
+
startIndex: options.startIndex
|
|
521
|
+
})
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function rebaseParameterSchemaWarnings(options) {
|
|
525
|
+
return options.warnings.map((warning) => {
|
|
526
|
+
if (warning.origin !== "schema-conversion") return warning;
|
|
527
|
+
const parameterIndex = options.parameterNames.findIndex((name) => isJsonPointerAtOrBelow(warning.schemaPath, `/properties/${escapeJsonPointerSegment(name)}`));
|
|
528
|
+
if (parameterIndex === -1) return warning;
|
|
529
|
+
const parameterName = options.parameterNames[parameterIndex];
|
|
530
|
+
if (parameterName === void 0) return warning;
|
|
531
|
+
const schemaPath = `/properties/${escapeJsonPointerSegment(parameterName)}`;
|
|
532
|
+
const suffix = warning.schemaPath.slice(schemaPath.length);
|
|
533
|
+
const documentPath = `${jsonPointer([
|
|
534
|
+
"paths",
|
|
535
|
+
options.context.openApiPath,
|
|
536
|
+
options.context.method,
|
|
537
|
+
"parameters",
|
|
538
|
+
String(options.startIndex + parameterIndex),
|
|
539
|
+
"schema"
|
|
540
|
+
])}${suffix}`;
|
|
541
|
+
return {
|
|
542
|
+
...warning,
|
|
543
|
+
documentPath,
|
|
544
|
+
location: {
|
|
545
|
+
...warning.location,
|
|
546
|
+
parameterName
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
function createParameterWarning(options) {
|
|
552
|
+
return {
|
|
553
|
+
origin: "openapi-builder",
|
|
554
|
+
code: options.code,
|
|
555
|
+
message: options.message,
|
|
556
|
+
documentPath: options.documentPath,
|
|
557
|
+
location: createOperationLocation(options)
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/internal/headerObjects.ts
|
|
562
|
+
function buildHeaderObjects(schema, context, responseContext) {
|
|
563
|
+
const container = extractParameterContainer({
|
|
564
|
+
schema,
|
|
565
|
+
context,
|
|
566
|
+
part: responseContext.part,
|
|
567
|
+
containerPointer: responseContext.headersPointer,
|
|
568
|
+
responseName: responseContext.responseName,
|
|
569
|
+
statusCode: responseContext.statusCode
|
|
570
|
+
});
|
|
571
|
+
return {
|
|
572
|
+
headers: Object.fromEntries(Object.entries(container.properties).map(([name, headerSchema]) => {
|
|
573
|
+
const headerPointer = `${responseContext.headersPointer}/${escapeJsonPointerSegment(name)}/schema`;
|
|
574
|
+
const description = headerDescription(headerSchema);
|
|
575
|
+
return [name, {
|
|
576
|
+
...description === void 0 ? {} : { description },
|
|
577
|
+
required: !container.isRootOptional && container.requiredNames.has(name),
|
|
578
|
+
schema: rebaseLocalJsonSchemaRefs(schemaWithoutDescription(headerSchema), headerPointer)
|
|
579
|
+
}];
|
|
580
|
+
})),
|
|
581
|
+
warnings: rebaseHeaderSchemaWarnings(container.warnings, Object.keys(container.properties), responseContext.headersPointer)
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function headerDescription(schema) {
|
|
585
|
+
return typeof schema.description === "string" && schema.description.trim() !== "" ? schema.description : void 0;
|
|
586
|
+
}
|
|
587
|
+
function schemaWithoutDescription(schema) {
|
|
588
|
+
return Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "description"));
|
|
589
|
+
}
|
|
590
|
+
function rebaseHeaderSchemaWarnings(warnings, headerNames, headersPointer) {
|
|
591
|
+
return warnings.map((warning) => {
|
|
592
|
+
if (warning.origin !== "schema-conversion") return warning;
|
|
593
|
+
const headerName = headerNames.find((name) => isJsonPointerAtOrBelow(warning.schemaPath, `/properties/${escapeJsonPointerSegment(name)}`));
|
|
594
|
+
if (headerName === void 0) return warning;
|
|
595
|
+
const schemaPath = `/properties/${escapeJsonPointerSegment(headerName)}`;
|
|
596
|
+
const suffix = warning.schemaPath.slice(schemaPath.length);
|
|
597
|
+
return {
|
|
598
|
+
...warning,
|
|
599
|
+
documentPath: `${headersPointer}/${escapeJsonPointerSegment(headerName)}/schema${suffix}`,
|
|
600
|
+
location: {
|
|
601
|
+
...warning.location,
|
|
602
|
+
parameterName: headerName
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
//#endregion
|
|
608
|
+
//#region src/internal/schemaRebasing.ts
|
|
609
|
+
function rebaseSchemaDocumentRefs(schema, fromPointer, toPointer) {
|
|
610
|
+
return rebaseSchemaValueDocumentRefs(schema, fromPointer, toPointer);
|
|
611
|
+
}
|
|
612
|
+
function rebaseWarningDocumentPath(warning, fromPointer, toPointer) {
|
|
613
|
+
if (warning.origin !== "schema-conversion" || !isJsonPointerAtOrBelow(warning.documentPath, fromPointer)) return warning;
|
|
614
|
+
return {
|
|
615
|
+
...warning,
|
|
616
|
+
documentPath: `${toPointer}${warning.documentPath.slice(fromPointer.length)}`
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function isWarningDocumentPathAtOrBelow(warning, pointer) {
|
|
620
|
+
return isJsonPointerAtOrBelow(warning.documentPath, pointer);
|
|
621
|
+
}
|
|
622
|
+
function rebaseSchemaValueDocumentRefs(value, fromPointer, toPointer) {
|
|
623
|
+
if (Array.isArray(value)) return value.map((item) => rebaseSchemaValueDocumentRefs(item, fromPointer, toPointer));
|
|
624
|
+
if (typeof value !== "object" || value === null) return value;
|
|
625
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, key === "$ref" && typeof child === "string" ? rebaseDocumentRef(child, fromPointer, toPointer) : rebaseSchemaValueDocumentRefs(child, fromPointer, toPointer)]));
|
|
626
|
+
}
|
|
627
|
+
function rebaseDocumentRef(ref, fromPointer, toPointer) {
|
|
628
|
+
if (!ref.startsWith("#")) return ref;
|
|
629
|
+
const documentPath = ref.slice(1);
|
|
630
|
+
if (!isJsonPointerAtOrBelow(documentPath, fromPointer)) return ref;
|
|
631
|
+
return `#${toPointer}${documentPath.slice(fromPointer.length)}`;
|
|
632
|
+
}
|
|
633
|
+
//#endregion
|
|
634
|
+
//#region src/internal/responseHeaderMerge.ts
|
|
635
|
+
function buildMergedHeaders(variants, options) {
|
|
636
|
+
const variantHeaders = variants.map((variant) => buildVariantHeaders(variant, options));
|
|
637
|
+
const mergedHeaders = headerNamesFrom(variantHeaders).map((name) => mergeHeader(name, variantHeaders, options.responsePointer));
|
|
638
|
+
return {
|
|
639
|
+
headers: Object.fromEntries(mergedHeaders.map((merged) => [merged.name, merged.header])),
|
|
640
|
+
warnings: [...warningsOutsideMergedHeaderSchemas(variantHeaders, options.responsePointer), ...mergedHeaders.flatMap((merged) => merged.warnings)]
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function buildVariantHeaders(variant, options) {
|
|
644
|
+
const built = buildHeaderObjects(variant.response.header, options.context, {
|
|
645
|
+
responseName: variant.usage.responseName,
|
|
646
|
+
statusCode: options.statusCode,
|
|
647
|
+
part: "response.header",
|
|
648
|
+
headersPointer: `${options.responsePointer}/headers`
|
|
649
|
+
});
|
|
650
|
+
return {
|
|
651
|
+
responseName: variant.usage.responseName,
|
|
652
|
+
headers: built.headers,
|
|
653
|
+
warnings: built.warnings
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function warningsOutsideMergedHeaderSchemas(variants, responsePointer) {
|
|
657
|
+
return variants.flatMap((variant) => {
|
|
658
|
+
const headerSchemaPointers = Object.keys(variant.headers).map((name) => headerSchemaPointer(responsePointer, name));
|
|
659
|
+
return variant.warnings.filter((warning) => !headerSchemaPointers.some((pointer) => isWarningDocumentPathAtOrBelow(warning, pointer)));
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
function headerNamesFrom(variants) {
|
|
663
|
+
const namesByLowercase = /* @__PURE__ */ new Map();
|
|
664
|
+
for (const variant of variants) for (const name of Object.keys(variant.headers)) {
|
|
665
|
+
const lowercaseName = name.toLowerCase();
|
|
666
|
+
if (!namesByLowercase.has(lowercaseName)) namesByLowercase.set(lowercaseName, name);
|
|
667
|
+
}
|
|
668
|
+
return [...namesByLowercase.values()];
|
|
669
|
+
}
|
|
670
|
+
function mergeHeader(name, variants, responsePointer) {
|
|
671
|
+
const schemaPointer = headerSchemaPointer(responsePointer, name);
|
|
672
|
+
const appearances = headerAppearancesFor(name, variants, responsePointer, schemaPointer);
|
|
673
|
+
const distinctSchemaAppearances = distinctHeaderSchemaAppearances(appearances);
|
|
674
|
+
const schema = mergedHeaderSchema(distinctSchemaAppearances, schemaPointer);
|
|
675
|
+
const warnings = mergedHeaderSchemaWarnings({
|
|
676
|
+
appearances,
|
|
677
|
+
distinctSchemaAppearances,
|
|
678
|
+
schemaPointer
|
|
679
|
+
});
|
|
680
|
+
const description = mergedHeaderDescription(appearances);
|
|
681
|
+
if (schema === void 0) return {
|
|
682
|
+
name,
|
|
683
|
+
header: {
|
|
684
|
+
required: false,
|
|
685
|
+
schema: {}
|
|
686
|
+
},
|
|
687
|
+
warnings
|
|
688
|
+
};
|
|
689
|
+
return {
|
|
690
|
+
name,
|
|
691
|
+
header: {
|
|
692
|
+
...description === void 0 ? {} : { description },
|
|
693
|
+
required: variants.every((_, index) => appearances.some((appearance) => appearance.variantIndex === index)) && appearances.every((appearance) => appearance.header.required),
|
|
694
|
+
schema
|
|
695
|
+
},
|
|
696
|
+
warnings
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function headerAppearancesFor(name, variants, responsePointer, schemaPointer) {
|
|
700
|
+
return variants.flatMap((variant, variantIndex) => {
|
|
701
|
+
const headerEntries = headerEntriesFor(variant.headers, name);
|
|
702
|
+
if (headerEntries.length === 0) return [];
|
|
703
|
+
return headerEntries.map((headerEntry) => {
|
|
704
|
+
const originalSchemaPointer = headerSchemaPointer(responsePointer, headerEntry.name);
|
|
705
|
+
const warnings = variant.warnings.filter((warning) => isWarningDocumentPathAtOrBelow(warning, originalSchemaPointer)).map((warning) => originalSchemaPointer === schemaPointer ? warning : rebaseWarningDocumentPath(warning, originalSchemaPointer, schemaPointer));
|
|
706
|
+
return {
|
|
707
|
+
variantIndex,
|
|
708
|
+
responseName: variant.responseName,
|
|
709
|
+
header: headerEntry.header,
|
|
710
|
+
warnings,
|
|
711
|
+
schemaKey: stableStringifyJsonSchema(headerEntry.header.schema)
|
|
712
|
+
};
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
function headerEntriesFor(headers, name) {
|
|
717
|
+
const lowerName = name.toLowerCase();
|
|
718
|
+
const entries = [];
|
|
719
|
+
for (const [headerName, header] of Object.entries(headers)) if (headerName.toLowerCase() === lowerName) entries.push({
|
|
720
|
+
name: headerName,
|
|
721
|
+
header
|
|
722
|
+
});
|
|
723
|
+
return entries;
|
|
724
|
+
}
|
|
725
|
+
function distinctHeaderSchemaAppearances(appearances) {
|
|
726
|
+
const seenSchemas = /* @__PURE__ */ new Set();
|
|
727
|
+
const distinctAppearances = [];
|
|
728
|
+
for (const appearance of appearances) {
|
|
729
|
+
if (seenSchemas.has(appearance.schemaKey)) continue;
|
|
730
|
+
seenSchemas.add(appearance.schemaKey);
|
|
731
|
+
distinctAppearances.push(appearance);
|
|
732
|
+
}
|
|
733
|
+
return distinctAppearances;
|
|
734
|
+
}
|
|
735
|
+
function mergedHeaderSchema(appearances, schemaPointer) {
|
|
736
|
+
const firstAppearance = appearances[0];
|
|
737
|
+
if (firstAppearance === void 0) return;
|
|
738
|
+
if (appearances.length === 1) return firstAppearance.header.schema;
|
|
739
|
+
return { anyOf: appearances.map((appearance, index) => rebaseSchemaDocumentRefs(appearance.header.schema, schemaPointer, `${schemaPointer}/anyOf/${index}`)) };
|
|
740
|
+
}
|
|
741
|
+
function mergedHeaderSchemaWarnings(options) {
|
|
742
|
+
if (options.distinctSchemaAppearances.length <= 1) return options.appearances.flatMap((appearance) => appearance.warnings);
|
|
743
|
+
return options.appearances.flatMap((appearance) => {
|
|
744
|
+
const schemaIndex = options.distinctSchemaAppearances.findIndex((distinctAppearance) => distinctAppearance.schemaKey === appearance.schemaKey);
|
|
745
|
+
const branchPointer = `${options.schemaPointer}/anyOf/${schemaIndex}`;
|
|
746
|
+
return appearance.warnings.map((warning) => rebaseWarningDocumentPath(warning, options.schemaPointer, branchPointer));
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
function headerSchemaPointer(responsePointer, name) {
|
|
750
|
+
return `${responsePointer}/headers/${escapeJsonPointerSegment(name)}/schema`;
|
|
751
|
+
}
|
|
752
|
+
function mergedHeaderDescription(appearances) {
|
|
753
|
+
const describedAppearances = appearances.filter((appearance) => appearance.header.description !== void 0);
|
|
754
|
+
const distinctDescriptions = new Set(describedAppearances.map((appearance) => appearance.header.description));
|
|
755
|
+
if (distinctDescriptions.size === 0) return;
|
|
756
|
+
if (distinctDescriptions.size === 1) return describedAppearances[0]?.header.description;
|
|
757
|
+
return ["Header description merged from response variants:", ...describedAppearances.map((appearance) => `- ${appearance.responseName}: ${appearance.header.description ?? ""}`)].join("\n");
|
|
758
|
+
}
|
|
759
|
+
function stableStringifyJsonSchema(schema) {
|
|
760
|
+
return JSON.stringify(canonicalizeJsonSchemaValue(schema));
|
|
761
|
+
}
|
|
762
|
+
function canonicalizeJsonSchemaValue(value) {
|
|
763
|
+
if (Array.isArray(value)) return value.map(canonicalizeJsonSchemaValue);
|
|
764
|
+
if (!isJsonSchemaObject(value)) return value;
|
|
765
|
+
return Object.fromEntries(Object.entries(value).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)).map(([key, child]) => [key, canonicalizeJsonSchemaValue(child)]));
|
|
766
|
+
}
|
|
767
|
+
function isJsonSchemaObject(value) {
|
|
768
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
769
|
+
}
|
|
770
|
+
//#endregion
|
|
771
|
+
//#region src/internal/responses.ts
|
|
772
|
+
function buildComponentsResponses(responses, schemaRegistry) {
|
|
773
|
+
const warnings = [];
|
|
774
|
+
return {
|
|
775
|
+
responses: Object.fromEntries(responses.map((response) => {
|
|
776
|
+
const responsePointer = jsonPointer([
|
|
777
|
+
"components",
|
|
778
|
+
"responses",
|
|
779
|
+
response.name
|
|
780
|
+
]);
|
|
781
|
+
const built = buildResponseObject(response, {
|
|
782
|
+
schemaRegistry,
|
|
783
|
+
responseName: response.name,
|
|
784
|
+
statusCode: String(response.statusCode),
|
|
785
|
+
responsePointer,
|
|
786
|
+
bodyBaseName: `${response.name}Body`
|
|
787
|
+
});
|
|
788
|
+
warnings.push(...built.warnings);
|
|
789
|
+
return [response.name, built.response];
|
|
790
|
+
})),
|
|
791
|
+
warnings
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
function buildOperationResponses(usages, canonicalResponsesByName, context, schemaRegistry) {
|
|
795
|
+
const responsesPointer = operationResponsesPointer(context);
|
|
796
|
+
const resolved = resolveResponseVariants(usages, {
|
|
797
|
+
canonicalResponsesByName,
|
|
798
|
+
context,
|
|
799
|
+
responsesPointer
|
|
800
|
+
});
|
|
801
|
+
const warnings = [...resolved.warnings];
|
|
802
|
+
const responses = {};
|
|
803
|
+
for (const [statusCode, variants] of groupResponsesByStatus(resolved.variants)) {
|
|
804
|
+
const built = buildResponseForStatus(variants, {
|
|
805
|
+
schemaRegistry,
|
|
806
|
+
context,
|
|
807
|
+
statusCode,
|
|
808
|
+
responsePointer: `${responsesPointer}/${statusCode}`
|
|
809
|
+
});
|
|
810
|
+
if (built === void 0) continue;
|
|
811
|
+
responses[statusCode] = built.response;
|
|
812
|
+
warnings.push(...built.warnings);
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
responses,
|
|
816
|
+
warnings
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function operationResponsesPointer(context) {
|
|
820
|
+
return jsonPointer([
|
|
821
|
+
"paths",
|
|
822
|
+
context.openApiPath,
|
|
823
|
+
context.method,
|
|
824
|
+
"responses"
|
|
825
|
+
]);
|
|
826
|
+
}
|
|
827
|
+
function resolveResponseVariants(usages, options) {
|
|
828
|
+
const warnings = [];
|
|
829
|
+
const variants = [];
|
|
830
|
+
for (const usage of usages) {
|
|
831
|
+
const response = resolveResponse(usage, options.canonicalResponsesByName);
|
|
832
|
+
if (response === void 0) {
|
|
833
|
+
warnings.push(createBuilderWarning({
|
|
834
|
+
code: "missing-canonical-response",
|
|
835
|
+
message: `Canonical response '${usage.responseName}' is not defined.`,
|
|
836
|
+
documentPath: options.responsesPointer,
|
|
837
|
+
context: options.context,
|
|
838
|
+
part: "response",
|
|
839
|
+
responseName: usage.responseName
|
|
840
|
+
}));
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
variants.push({
|
|
844
|
+
response,
|
|
845
|
+
usage,
|
|
846
|
+
statusCode: String(response.statusCode)
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
variants,
|
|
851
|
+
warnings
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
function buildResponseForStatus(variants, options) {
|
|
855
|
+
if (variants.length === 1) {
|
|
856
|
+
const variant = variants[0];
|
|
857
|
+
return variant === void 0 ? void 0 : buildSingleResponseVariant(variant, options);
|
|
858
|
+
}
|
|
859
|
+
return buildMergedResponseObject(variants, {
|
|
860
|
+
schemaRegistry: options.schemaRegistry,
|
|
861
|
+
context: options.context,
|
|
862
|
+
statusCode: options.statusCode,
|
|
863
|
+
responsePointer: options.responsePointer
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
function buildSingleResponseVariant(variant, options) {
|
|
867
|
+
if (variant.usage.source === "canonical") return {
|
|
868
|
+
response: { $ref: `#/components/responses/${escapeJsonPointerSegment(variant.usage.responseName)}` },
|
|
869
|
+
warnings: []
|
|
870
|
+
};
|
|
871
|
+
const built = buildResponseObject(variant.response, {
|
|
872
|
+
schemaRegistry: options.schemaRegistry,
|
|
873
|
+
context: options.context,
|
|
874
|
+
responseName: variant.usage.responseName,
|
|
875
|
+
statusCode: options.statusCode,
|
|
876
|
+
responsePointer: options.responsePointer,
|
|
877
|
+
bodyBaseName: inlineResponseBodyBaseName(options.context, variant.usage.responseName)
|
|
878
|
+
});
|
|
879
|
+
return {
|
|
880
|
+
response: built.response,
|
|
881
|
+
warnings: built.warnings
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
function groupResponsesByStatus(variants) {
|
|
885
|
+
const groups = /* @__PURE__ */ new Map();
|
|
886
|
+
for (const variant of variants) {
|
|
887
|
+
const group = groups.get(variant.statusCode);
|
|
888
|
+
if (group === void 0) {
|
|
889
|
+
groups.set(variant.statusCode, [variant]);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
group.push(variant);
|
|
893
|
+
}
|
|
894
|
+
return groups;
|
|
895
|
+
}
|
|
896
|
+
function resolveResponse(usage, canonicalResponsesByName) {
|
|
897
|
+
return usage.source === "inline" ? usage.response : canonicalResponsesByName.get(usage.responseName);
|
|
898
|
+
}
|
|
899
|
+
function buildResponseObject(response, options) {
|
|
900
|
+
const context = options.context ?? createComponentResponseContext(options.responseName);
|
|
901
|
+
const headers = buildHeaderObjects(response.header, context, {
|
|
902
|
+
responseName: options.responseName,
|
|
903
|
+
statusCode: options.statusCode,
|
|
904
|
+
part: "response.header",
|
|
905
|
+
headersPointer: `${options.responsePointer}/headers`
|
|
906
|
+
});
|
|
907
|
+
const body = buildResponseBody(response, {
|
|
908
|
+
schemaRegistry: options.schemaRegistry,
|
|
909
|
+
context,
|
|
910
|
+
responseName: options.responseName,
|
|
911
|
+
statusCode: options.statusCode,
|
|
912
|
+
baseName: options.bodyBaseName
|
|
913
|
+
});
|
|
914
|
+
return {
|
|
915
|
+
response: {
|
|
916
|
+
description: response.description,
|
|
917
|
+
...Object.keys(headers.headers).length > 0 ? { headers: headers.headers } : {},
|
|
918
|
+
...body.content === void 0 ? {} : { content: body.content }
|
|
919
|
+
},
|
|
920
|
+
warnings: [...headers.warnings, ...body.warnings]
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function buildResponseBody(response, options) {
|
|
924
|
+
const body = response.body;
|
|
925
|
+
if (body === void 0) return { warnings: [] };
|
|
926
|
+
const registration = registerResponseBody(body, {
|
|
927
|
+
schemaRegistry: options.schemaRegistry,
|
|
928
|
+
context: options.context,
|
|
929
|
+
responseName: options.responseName,
|
|
930
|
+
statusCode: options.statusCode,
|
|
931
|
+
baseName: options.baseName
|
|
932
|
+
});
|
|
933
|
+
return {
|
|
934
|
+
content: { [body.mediaType]: { schema: registration.schema } },
|
|
935
|
+
warnings: registration.warnings
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function buildMergedResponseObject(variants, options) {
|
|
939
|
+
const body = buildMergedResponseBody(variants, options);
|
|
940
|
+
const headers = buildMergedHeaders(variants, options);
|
|
941
|
+
return {
|
|
942
|
+
response: {
|
|
943
|
+
description: variants.map((variant) => `${variant.usage.responseName}: ${variant.response.description}`).join("\n\n"),
|
|
944
|
+
...Object.keys(headers.headers).length > 0 ? { headers: headers.headers } : {},
|
|
945
|
+
...body.content === void 0 ? {} : { content: body.content }
|
|
946
|
+
},
|
|
947
|
+
warnings: [...body.warnings, ...headers.warnings]
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function buildMergedResponseBody(variants, options) {
|
|
951
|
+
const warnings = [];
|
|
952
|
+
const schemasByMediaType = /* @__PURE__ */ new Map();
|
|
953
|
+
for (const variant of variants) {
|
|
954
|
+
const body = variant.response.body;
|
|
955
|
+
if (body === void 0) continue;
|
|
956
|
+
const registration = registerResponseBody(body, {
|
|
957
|
+
schemaRegistry: options.schemaRegistry,
|
|
958
|
+
context: options.context,
|
|
959
|
+
baseName: responseBodyBaseName(options.context, variant.usage),
|
|
960
|
+
responseName: variant.usage.responseName,
|
|
961
|
+
statusCode: options.statusCode
|
|
962
|
+
});
|
|
963
|
+
const schemas = schemasByMediaType.get(body.mediaType) ?? [];
|
|
964
|
+
schemasByMediaType.set(body.mediaType, [...schemas, registration]);
|
|
965
|
+
warnings.push(...registration.warnings);
|
|
966
|
+
}
|
|
967
|
+
if (schemasByMediaType.size === 0) return { warnings };
|
|
968
|
+
return {
|
|
969
|
+
content: Object.fromEntries(Array.from(schemasByMediaType, ([mediaType, registrations]) => [mediaType, { schema: mergedMediaTypeSchema(registrations) }])),
|
|
970
|
+
warnings
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function mergedMediaTypeSchema(registrations) {
|
|
974
|
+
const distinctSchemas = distinctBy(registrations, (registration) => registration.schemaKey).map((registration) => registration.schema);
|
|
975
|
+
const firstSchema = distinctSchemas[0];
|
|
976
|
+
if (firstSchema === void 0) return {};
|
|
977
|
+
return distinctSchemas.slice(1).length === 0 ? firstSchema : { anyOf: distinctSchemas };
|
|
978
|
+
}
|
|
979
|
+
function registerResponseBody(body, options) {
|
|
980
|
+
const resolvedSchema = resolveOpenApiBodySchema(body, () => {
|
|
981
|
+
const registration = options.schemaRegistry.register({
|
|
982
|
+
schema: body.schema,
|
|
983
|
+
baseName: options.baseName,
|
|
984
|
+
location: createOperationLocation({
|
|
985
|
+
context: options.context,
|
|
986
|
+
part: "response.body",
|
|
987
|
+
responseName: options.responseName,
|
|
988
|
+
statusCode: options.statusCode
|
|
989
|
+
})
|
|
990
|
+
});
|
|
991
|
+
return {
|
|
992
|
+
schema: registration.ref,
|
|
993
|
+
schemaKey: registration.ref.$ref,
|
|
994
|
+
warnings: registration.warnings
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
return {
|
|
998
|
+
mediaType: body.mediaType,
|
|
999
|
+
...resolvedSchema
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function responseBodyBaseName(context, usage) {
|
|
1003
|
+
return usage.source === "canonical" ? `${usage.responseName}Body` : inlineResponseBodyBaseName(context, usage.responseName);
|
|
1004
|
+
}
|
|
1005
|
+
function inlineResponseBodyBaseName(context, responseName) {
|
|
1006
|
+
return `${pascalCase(context.operation.operationId)}${responseName}Body`;
|
|
1007
|
+
}
|
|
1008
|
+
function distinctBy(values, keyForValue) {
|
|
1009
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1010
|
+
const distinctValues = [];
|
|
1011
|
+
for (const value of values) {
|
|
1012
|
+
const key = keyForValue(value);
|
|
1013
|
+
if (seen.has(key)) continue;
|
|
1014
|
+
seen.add(key);
|
|
1015
|
+
distinctValues.push(value);
|
|
1016
|
+
}
|
|
1017
|
+
return distinctValues;
|
|
1018
|
+
}
|
|
1019
|
+
function createComponentResponseContext(responseName) {
|
|
1020
|
+
return {
|
|
1021
|
+
resourceName: "components.responses",
|
|
1022
|
+
operation: {
|
|
1023
|
+
operationId: responseName,
|
|
1024
|
+
method: "components",
|
|
1025
|
+
path: "#/components/responses"
|
|
1026
|
+
},
|
|
1027
|
+
openApiPath: "#/components/responses",
|
|
1028
|
+
method: "components"
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
function createBuilderWarning(options) {
|
|
1032
|
+
return {
|
|
1033
|
+
origin: "openapi-builder",
|
|
1034
|
+
code: options.code,
|
|
1035
|
+
message: options.message,
|
|
1036
|
+
documentPath: options.documentPath,
|
|
1037
|
+
location: createOperationLocation(options)
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
//#endregion
|
|
1041
|
+
//#region src/internal/schemaRegistry.ts
|
|
1042
|
+
function createSchemaRegistry() {
|
|
1043
|
+
const entriesBySchema = /* @__PURE__ */ new WeakMap();
|
|
1044
|
+
const components = /* @__PURE__ */ new Map();
|
|
1045
|
+
return {
|
|
1046
|
+
register: (options) => {
|
|
1047
|
+
const schema = unwrapRootOptional(options.schema).schema;
|
|
1048
|
+
const existingEntry = entriesBySchema.get(schema);
|
|
1049
|
+
if (existingEntry !== void 0) return {
|
|
1050
|
+
name: existingEntry.name,
|
|
1051
|
+
ref: existingEntry.ref,
|
|
1052
|
+
warnings: []
|
|
1053
|
+
};
|
|
1054
|
+
const name = nextComponentName(options.baseName, components);
|
|
1055
|
+
const converted = convertSchema(schema, jsonPointer([
|
|
1056
|
+
"components",
|
|
1057
|
+
"schemas",
|
|
1058
|
+
name
|
|
1059
|
+
]), options.location);
|
|
1060
|
+
const ref = { $ref: `#/components/schemas/${escapeJsonPointerSegment(name)}` };
|
|
1061
|
+
const entry = {
|
|
1062
|
+
name,
|
|
1063
|
+
ref,
|
|
1064
|
+
schema: converted.schema
|
|
1065
|
+
};
|
|
1066
|
+
entriesBySchema.set(schema, entry);
|
|
1067
|
+
components.set(name, converted.schema);
|
|
1068
|
+
return {
|
|
1069
|
+
name,
|
|
1070
|
+
ref,
|
|
1071
|
+
warnings: converted.warnings
|
|
1072
|
+
};
|
|
1073
|
+
},
|
|
1074
|
+
components: () => Object.fromEntries(components)
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function nextComponentName(baseName, components) {
|
|
1078
|
+
const sanitizedName = sanitizeComponentName(baseName);
|
|
1079
|
+
if (!components.has(sanitizedName)) return sanitizedName;
|
|
1080
|
+
for (let suffix = 2;; suffix += 1) {
|
|
1081
|
+
const name = `${sanitizedName}_${suffix}`;
|
|
1082
|
+
if (!components.has(name)) return name;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
function sanitizeComponentName(name) {
|
|
1086
|
+
const sanitizedName = name.trim().replaceAll(/[^A-Za-z0-9._-]/g, "_").replaceAll(/_+/g, "_").replaceAll(/^_+|_+$/g, "");
|
|
1087
|
+
return sanitizedName === "" ? "Schema" : sanitizedName;
|
|
1088
|
+
}
|
|
1089
|
+
//#endregion
|
|
1090
|
+
//#region src/buildOpenApiDocument.ts
|
|
1091
|
+
function buildOpenApiDocument(normalizedSpec, options) {
|
|
1092
|
+
const warnings = [...duplicateCanonicalResponseWarnings(normalizedSpec.responses)];
|
|
1093
|
+
const schemaRegistry = createSchemaRegistry();
|
|
1094
|
+
const canonicalResponses = buildComponentsResponses(normalizedSpec.responses, schemaRegistry);
|
|
1095
|
+
const canonicalResponsesByName = new Map(normalizedSpec.responses.map((response) => [response.name, response]));
|
|
1096
|
+
const paths = {};
|
|
1097
|
+
warnings.push(...canonicalResponses.warnings);
|
|
1098
|
+
for (const resource of normalizedSpec.resources) for (const operation of resource.operations) {
|
|
1099
|
+
const method = operation.method.toLowerCase();
|
|
1100
|
+
const openApiPath = toOpenApiPath(operation.path);
|
|
1101
|
+
const operationObject = buildOperationObject({
|
|
1102
|
+
resourceName: resource.name,
|
|
1103
|
+
operation,
|
|
1104
|
+
openApiPath,
|
|
1105
|
+
method,
|
|
1106
|
+
canonicalResponsesByName,
|
|
1107
|
+
schemaRegistry
|
|
1108
|
+
});
|
|
1109
|
+
paths[openApiPath] = {
|
|
1110
|
+
...paths[openApiPath],
|
|
1111
|
+
[method]: operationObject.operation
|
|
1112
|
+
};
|
|
1113
|
+
warnings.push(...operationObject.warnings);
|
|
1114
|
+
}
|
|
1115
|
+
const schemas = schemaRegistry.components();
|
|
1116
|
+
const responses = canonicalResponses.responses;
|
|
1117
|
+
const hasResponses = Object.keys(responses).length > 0;
|
|
1118
|
+
const hasSchemas = Object.keys(schemas).length > 0;
|
|
1119
|
+
return {
|
|
1120
|
+
document: {
|
|
1121
|
+
openapi: "3.1.1",
|
|
1122
|
+
jsonSchemaDialect: "https://json-schema.org/draft/2020-12/schema",
|
|
1123
|
+
info: { ...options.info },
|
|
1124
|
+
...options.servers === void 0 ? {} : { servers: [...options.servers] },
|
|
1125
|
+
tags: normalizedSpec.resources.map((resource) => ({ name: resource.name })),
|
|
1126
|
+
paths,
|
|
1127
|
+
...!hasResponses && !hasSchemas ? {} : { components: {
|
|
1128
|
+
...hasResponses ? { responses } : {},
|
|
1129
|
+
...hasSchemas ? { schemas } : {}
|
|
1130
|
+
} }
|
|
1131
|
+
},
|
|
1132
|
+
warnings
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function buildOperationObject(options) {
|
|
1136
|
+
const context = {
|
|
1137
|
+
resourceName: options.resourceName,
|
|
1138
|
+
operation: options.operation,
|
|
1139
|
+
openApiPath: options.openApiPath,
|
|
1140
|
+
method: options.method
|
|
1141
|
+
};
|
|
1142
|
+
const parameters = buildRequestParameters(context);
|
|
1143
|
+
const requestBody = buildRequestBody(context, options.schemaRegistry);
|
|
1144
|
+
const responses = buildOperationResponses(options.operation.responses, options.canonicalResponsesByName, context, options.schemaRegistry);
|
|
1145
|
+
return {
|
|
1146
|
+
operation: {
|
|
1147
|
+
operationId: options.operation.operationId,
|
|
1148
|
+
...options.operation.summary.trim() === "" ? {} : { summary: options.operation.summary },
|
|
1149
|
+
tags: [options.resourceName],
|
|
1150
|
+
...parameters.parameters.length === 0 ? {} : { parameters: parameters.parameters },
|
|
1151
|
+
...requestBody.requestBody === void 0 ? {} : { requestBody: requestBody.requestBody },
|
|
1152
|
+
responses: responses.responses
|
|
1153
|
+
},
|
|
1154
|
+
warnings: [
|
|
1155
|
+
...parameters.warnings,
|
|
1156
|
+
...requestBody.warnings,
|
|
1157
|
+
...responses.warnings
|
|
1158
|
+
]
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
function buildRequestBody(context, schemaRegistry) {
|
|
1162
|
+
const body = context.operation.request?.body;
|
|
1163
|
+
if (body === void 0) return { warnings: [] };
|
|
1164
|
+
const optionalSchema = unwrapRootOptional(body.schema);
|
|
1165
|
+
const resolvedSchema = resolveOpenApiBodySchema(body, () => {
|
|
1166
|
+
const registration = schemaRegistry.register({
|
|
1167
|
+
schema: optionalSchema.schema,
|
|
1168
|
+
baseName: `${pascalCase(context.operation.operationId)}RequestBody`,
|
|
1169
|
+
location: {
|
|
1170
|
+
resourceName: context.resourceName,
|
|
1171
|
+
operationId: context.operation.operationId,
|
|
1172
|
+
method: context.operation.method,
|
|
1173
|
+
path: context.operation.path,
|
|
1174
|
+
openApiPath: context.openApiPath,
|
|
1175
|
+
part: "request.body"
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
return {
|
|
1179
|
+
schema: registration.ref,
|
|
1180
|
+
schemaKey: registration.ref.$ref,
|
|
1181
|
+
warnings: registration.warnings
|
|
1182
|
+
};
|
|
1183
|
+
});
|
|
1184
|
+
return {
|
|
1185
|
+
requestBody: {
|
|
1186
|
+
required: !optionalSchema.isOptional,
|
|
1187
|
+
content: { [body.mediaType]: { schema: resolvedSchema.schema } }
|
|
1188
|
+
},
|
|
1189
|
+
warnings: resolvedSchema.warnings
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
function duplicateCanonicalResponseWarnings(responses) {
|
|
1193
|
+
const firstSeenAt = /* @__PURE__ */ new Map();
|
|
1194
|
+
const warnings = [];
|
|
1195
|
+
responses.forEach((response, index) => {
|
|
1196
|
+
const previousIndex = firstSeenAt.get(response.name);
|
|
1197
|
+
if (previousIndex === void 0) {
|
|
1198
|
+
firstSeenAt.set(response.name, index);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
warnings.push({
|
|
1202
|
+
origin: "openapi-builder",
|
|
1203
|
+
code: "duplicate-canonical-response",
|
|
1204
|
+
message: `Canonical response '${response.name}' is defined more than once; the entry at index ${index} overrides the entry at index ${previousIndex}.`,
|
|
1205
|
+
documentPath: jsonPointer([
|
|
1206
|
+
"components",
|
|
1207
|
+
"responses",
|
|
1208
|
+
response.name
|
|
1209
|
+
]),
|
|
1210
|
+
location: {
|
|
1211
|
+
responseName: response.name,
|
|
1212
|
+
part: "components.responses"
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
return warnings;
|
|
1217
|
+
}
|
|
1218
|
+
//#endregion
|
|
1219
|
+
//#region src/OpenApiPlugin.ts
|
|
1220
|
+
const DEFAULT_INFO = {
|
|
1221
|
+
title: "Typeweaver API",
|
|
1222
|
+
version: "0.0.0"
|
|
1223
|
+
};
|
|
1224
|
+
const DEFAULT_OUTPUT_PATH = "openapi/openapi.json";
|
|
1225
|
+
var OpenApiPlugin = class extends BasePlugin {
|
|
1226
|
+
name = "openapi";
|
|
1227
|
+
options;
|
|
1228
|
+
constructor(options = {}) {
|
|
1229
|
+
super({});
|
|
1230
|
+
this.options = normalizeOptions(validateOptions(options));
|
|
1231
|
+
}
|
|
1232
|
+
generate(context) {
|
|
1233
|
+
const result = buildOpenApiDocument(context.normalizedSpec, {
|
|
1234
|
+
info: this.options.info,
|
|
1235
|
+
servers: this.options.servers
|
|
1236
|
+
});
|
|
1237
|
+
const json = `${JSON.stringify(result.document, null, 2)}\n`;
|
|
1238
|
+
context.writeFile(this.options.outputPath, json);
|
|
1239
|
+
if (result.warnings.length > 0) console.warn(formatWarnings(result.warnings));
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
function validateOptions(options) {
|
|
1243
|
+
if (!isPlainObject(options)) throwConfigError("options must be an object");
|
|
1244
|
+
return options;
|
|
1245
|
+
}
|
|
1246
|
+
function normalizeOptions(options) {
|
|
1247
|
+
const outputPath = options.outputPath === void 0 ? DEFAULT_OUTPUT_PATH : options.outputPath;
|
|
1248
|
+
return {
|
|
1249
|
+
info: normalizeInfo(options.info),
|
|
1250
|
+
...options.servers === void 0 ? {} : { servers: normalizeServers(options.servers) },
|
|
1251
|
+
outputPath: normalizeOutputPath(outputPath)
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function normalizeInfo(info) {
|
|
1255
|
+
if (info === void 0) return { ...DEFAULT_INFO };
|
|
1256
|
+
if (!isPlainObject(info)) throwConfigError("info must be an object with string title and version");
|
|
1257
|
+
if (typeof info.title !== "string" || typeof info.version !== "string") throwConfigError("info.title and info.version must be strings");
|
|
1258
|
+
return { ...info };
|
|
1259
|
+
}
|
|
1260
|
+
function normalizeServers(servers) {
|
|
1261
|
+
if (!Array.isArray(servers)) throwConfigError("servers must be an array of objects with string url");
|
|
1262
|
+
return servers.map((server, index) => {
|
|
1263
|
+
if (!isOpenApiServerObject(server)) throwConfigError(`servers[${index}].url must be a string`);
|
|
1264
|
+
return { ...server };
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
function normalizeOutputPath(outputPath) {
|
|
1268
|
+
if (typeof outputPath !== "string" || outputPath.length === 0) throwConfigError("outputPath must be a non-empty relative .json path");
|
|
1269
|
+
if (!outputPath.endsWith(".json")) throwConfigError("outputPath must end with .json");
|
|
1270
|
+
if (outputPath.includes("\0")) throwConfigError("outputPath must not contain null bytes");
|
|
1271
|
+
if (path.isAbsolute(outputPath) || path.win32.isAbsolute(outputPath)) throwConfigError("outputPath must be relative");
|
|
1272
|
+
const pathSegments = outputPath.replace(/\\/g, "/").split("/");
|
|
1273
|
+
if (pathSegments.some((segment) => segment === "..")) throwConfigError("outputPath must not contain parent directory segments");
|
|
1274
|
+
const normalizedPath = path.posix.normalize(pathSegments.join("/"));
|
|
1275
|
+
if (normalizedPath === "." || normalizedPath.startsWith("../")) throwConfigError("outputPath must be a safe relative .json path");
|
|
1276
|
+
return normalizedPath;
|
|
1277
|
+
}
|
|
1278
|
+
function isPlainObject(value) {
|
|
1279
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1280
|
+
}
|
|
1281
|
+
function isOpenApiServerObject(value) {
|
|
1282
|
+
return isPlainObject(value) && typeof value.url === "string";
|
|
1283
|
+
}
|
|
1284
|
+
function throwConfigError(message) {
|
|
1285
|
+
throw new Error(`OpenApiPlugin config error: ${message}`);
|
|
1286
|
+
}
|
|
1287
|
+
function formatWarnings(warnings) {
|
|
1288
|
+
const warningLines = warnings.map((warning) => `- ${warning.code}: ${warning.message} (${warning.documentPath})`);
|
|
1289
|
+
return [`OpenAPI generation completed with ${warnings.length} warning(s).`, ...warningLines].join("\n");
|
|
1290
|
+
}
|
|
1291
|
+
//#endregion
|
|
1292
|
+
export { OpenApiPlugin, buildOpenApiDocument };
|
|
1293
|
+
|
|
1294
|
+
//# sourceMappingURL=index.mjs.map
|