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