@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/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