apidoc-to-openapi 0.1.1

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.
@@ -0,0 +1,1197 @@
1
+ import YAML from "yaml";
2
+
3
+ const VALID_METHODS = new Set([
4
+ "get",
5
+ "post",
6
+ "put",
7
+ "patch",
8
+ "delete",
9
+ "head",
10
+ "options",
11
+ "trace",
12
+ ]);
13
+
14
+ const BODYLESS_METHODS = new Set(["get", "head", "delete", "options", "trace"]);
15
+
16
+ const TYPE_MAP = {
17
+ string: { type: "string" },
18
+ number: { type: "number" },
19
+ integer: { type: "integer" },
20
+ int: { type: "integer" },
21
+ long: { type: "integer" },
22
+ float: { type: "number" },
23
+ double: { type: "number" },
24
+ boolean: { type: "boolean" },
25
+ bool: { type: "boolean" },
26
+ object: { type: "object", properties: {} },
27
+ array: { type: "array", items: { type: "string" } },
28
+ file: { type: "string", format: "binary" },
29
+ date: { type: "string", format: "date" },
30
+ datetime: { type: "string", format: "date-time" },
31
+ "date-time": { type: "string", format: "date-time" },
32
+ };
33
+
34
+ const ARRAY_TYPE_ALIASES = new Set([
35
+ "array",
36
+ "list",
37
+ "arraylist",
38
+ "linkedlist",
39
+ "set",
40
+ "hashset",
41
+ ]);
42
+
43
+ const OBJECT_TYPE_ALIASES = new Set([
44
+ "object",
45
+ "map",
46
+ "hashmap",
47
+ "linkedhashmap",
48
+ "dictionary",
49
+ "dict",
50
+ "record",
51
+ ]);
52
+
53
+ function sanitizeText(input) {
54
+ if (typeof input !== "string" || input.trim() === "") {
55
+ return "";
56
+ }
57
+
58
+ const noTags = input.replace(/<[^>]*>/g, " ");
59
+ return noTags
60
+ .replace(/&nbsp;/g, " ")
61
+ .replace(/&lt;/g, "<")
62
+ .replace(/&gt;/g, ">")
63
+ .replace(/&amp;/g, "&")
64
+ .replace(/&quot;/g, '"')
65
+ .replace(/&#39;/g, "'")
66
+ .replace(/\s+/g, " ")
67
+ .trim();
68
+ }
69
+
70
+ function toOpenApiPath(url) {
71
+ if (typeof url !== "string" || url.trim() === "") {
72
+ return "/";
73
+ }
74
+
75
+ const withoutQuery = url.split("?")[0].trim();
76
+ const withLeadingSlash = withoutQuery.startsWith("/")
77
+ ? withoutQuery
78
+ : `/${withoutQuery}`;
79
+
80
+ return withLeadingSlash.replace(/:([A-Za-z0-9_]+)/g, "{$1}");
81
+ }
82
+
83
+ function extractPathParams(pathname) {
84
+ const names = [];
85
+ const pattern = /\{([^}]+)\}/g;
86
+ let match = pattern.exec(pathname);
87
+
88
+ while (match) {
89
+ names.push(match[1]);
90
+ match = pattern.exec(pathname);
91
+ }
92
+
93
+ return names;
94
+ }
95
+
96
+ function normalizeFieldName(fieldName) {
97
+ if (typeof fieldName !== "string") {
98
+ return "";
99
+ }
100
+ return fieldName.trim().replace(/^:/, "");
101
+ }
102
+
103
+ function normalizePathParamField(fieldName) {
104
+ return normalizeFieldName(fieldName)
105
+ .replace(/\[\]$/g, "")
106
+ .split(".")[0];
107
+ }
108
+
109
+ function normalizeTypeToken(typeName) {
110
+ if (typeof typeName !== "string") {
111
+ return "";
112
+ }
113
+
114
+ const trimmed = typeName.trim().replace(/\?/g, "");
115
+ if (!trimmed) {
116
+ return "";
117
+ }
118
+
119
+ const parts = trimmed.split(".");
120
+ const token = parts[parts.length - 1] || trimmed;
121
+ return token.trim().toLowerCase();
122
+ }
123
+
124
+ function safeClone(value) {
125
+ return JSON.parse(JSON.stringify(value));
126
+ }
127
+
128
+ function splitTopLevel(input, separator) {
129
+ const parts = [];
130
+ let buffer = "";
131
+ let angleDepth = 0;
132
+ let parenDepth = 0;
133
+ let bracketDepth = 0;
134
+
135
+ for (const char of `${input}`) {
136
+ if (char === "<") {
137
+ angleDepth += 1;
138
+ } else if (char === ">") {
139
+ angleDepth = Math.max(0, angleDepth - 1);
140
+ } else if (char === "(") {
141
+ parenDepth += 1;
142
+ } else if (char === ")") {
143
+ parenDepth = Math.max(0, parenDepth - 1);
144
+ } else if (char === "[") {
145
+ bracketDepth += 1;
146
+ } else if (char === "]") {
147
+ bracketDepth = Math.max(0, bracketDepth - 1);
148
+ }
149
+
150
+ if (
151
+ char === separator &&
152
+ angleDepth === 0 &&
153
+ parenDepth === 0 &&
154
+ bracketDepth === 0
155
+ ) {
156
+ const part = buffer.trim();
157
+ if (part) {
158
+ parts.push(part);
159
+ }
160
+ buffer = "";
161
+ continue;
162
+ }
163
+
164
+ buffer += char;
165
+ }
166
+
167
+ const tail = buffer.trim();
168
+ if (tail) {
169
+ parts.push(tail);
170
+ }
171
+
172
+ return parts;
173
+ }
174
+
175
+ function peelArraySuffix(typeExpression) {
176
+ let expression = `${typeExpression}`.trim();
177
+ let depth = 0;
178
+
179
+ while (expression.endsWith("[]")) {
180
+ depth += 1;
181
+ expression = expression.slice(0, -2).trim();
182
+ }
183
+
184
+ return { expression, depth };
185
+ }
186
+
187
+ function splitGenericType(typeExpression) {
188
+ const source = `${typeExpression}`.trim();
189
+ const ltIndex = source.indexOf("<");
190
+ if (ltIndex === -1) {
191
+ return null;
192
+ }
193
+
194
+ let depth = 0;
195
+ let closeIndex = -1;
196
+ for (let index = ltIndex; index < source.length; index += 1) {
197
+ const char = source[index];
198
+ if (char === "<") {
199
+ depth += 1;
200
+ } else if (char === ">") {
201
+ depth -= 1;
202
+ if (depth === 0) {
203
+ closeIndex = index;
204
+ break;
205
+ }
206
+ }
207
+ }
208
+
209
+ if (closeIndex === -1) {
210
+ return null;
211
+ }
212
+
213
+ if (source.slice(closeIndex + 1).trim() !== "") {
214
+ return null;
215
+ }
216
+
217
+ const outer = source.slice(0, ltIndex).trim().replace(/\.$/, "");
218
+ const inner = source.slice(ltIndex + 1, closeIndex).trim();
219
+ if (!outer || !inner) {
220
+ return null;
221
+ }
222
+
223
+ return { outer, inner };
224
+ }
225
+
226
+ function schemaFromTypeCore(typeExpression) {
227
+ const generic = splitGenericType(typeExpression);
228
+ if (generic) {
229
+ const outer = normalizeTypeToken(generic.outer);
230
+ const genericArgs = splitTopLevel(generic.inner, ",");
231
+
232
+ if (ARRAY_TYPE_ALIASES.has(outer)) {
233
+ const itemType = genericArgs[0] || "string";
234
+ return {
235
+ type: "array",
236
+ items: schemaFromTypeExpression(itemType),
237
+ };
238
+ }
239
+
240
+ if (OBJECT_TYPE_ALIASES.has(outer)) {
241
+ const schema = { type: "object", properties: {} };
242
+ if (genericArgs.length >= 2) {
243
+ schema.additionalProperties = schemaFromTypeExpression(genericArgs[1]);
244
+ }
245
+ return schema;
246
+ }
247
+ }
248
+
249
+ const normalized = normalizeTypeToken(typeExpression);
250
+ if (ARRAY_TYPE_ALIASES.has(normalized)) {
251
+ return { type: "array", items: { type: "string" } };
252
+ }
253
+ if (OBJECT_TYPE_ALIASES.has(normalized)) {
254
+ return { type: "object", properties: {} };
255
+ }
256
+
257
+ if (TYPE_MAP[normalized]) {
258
+ return safeClone(TYPE_MAP[normalized]);
259
+ }
260
+
261
+ return { type: "string" };
262
+ }
263
+
264
+ function schemaFromTypeExpression(typeName) {
265
+ if (typeof typeName !== "string" || typeName.trim() === "") {
266
+ return { type: "string" };
267
+ }
268
+
269
+ let expression = typeName.trim();
270
+ const unionParts = splitTopLevel(expression, "|");
271
+ if (unionParts.length > 1) {
272
+ expression = unionParts[0];
273
+ }
274
+
275
+ const { expression: peeledExpression, depth: arrayDepth } = peelArraySuffix(
276
+ expression,
277
+ );
278
+ let schema = schemaFromTypeCore(peeledExpression);
279
+
280
+ for (let index = 0; index < arrayDepth; index += 1) {
281
+ schema = { type: "array", items: schema };
282
+ }
283
+
284
+ return schema;
285
+ }
286
+
287
+ function parseAllowedValues(raw) {
288
+ if (!raw) {
289
+ return [];
290
+ }
291
+
292
+ if (Array.isArray(raw)) {
293
+ return raw.filter((item) => item !== undefined && item !== null);
294
+ }
295
+
296
+ if (typeof raw === "string") {
297
+ return raw
298
+ .split(",")
299
+ .map((item) => item.trim())
300
+ .filter(Boolean);
301
+ }
302
+
303
+ return [];
304
+ }
305
+
306
+ function parseEnumToken(token) {
307
+ const text = `${token || ""}`.trim();
308
+ if (!text) {
309
+ return null;
310
+ }
311
+
312
+ const [rawValue, ...labelParts] = text.split("|");
313
+ const valueText = `${rawValue || ""}`.trim();
314
+ if (!valueText) {
315
+ return null;
316
+ }
317
+
318
+ const label = labelParts.join("|").trim();
319
+ return {
320
+ value: parsePrimitive(valueText),
321
+ description: label || "",
322
+ };
323
+ }
324
+
325
+ function dedupeEnumEntries(entries) {
326
+ const unique = [];
327
+ const seen = new Set();
328
+
329
+ for (const entry of entries) {
330
+ if (!entry) {
331
+ continue;
332
+ }
333
+
334
+ const value = entry.value;
335
+ const key = `${typeof value}:${JSON.stringify(value)}`;
336
+ if (seen.has(key)) {
337
+ continue;
338
+ }
339
+ seen.add(key);
340
+ unique.push(entry);
341
+ }
342
+
343
+ return unique;
344
+ }
345
+
346
+ function parseEnumEntries(raw) {
347
+ if (!raw) {
348
+ return [];
349
+ }
350
+
351
+ const entries = [];
352
+ if (Array.isArray(raw)) {
353
+ for (const item of raw) {
354
+ if (typeof item === "string") {
355
+ entries.push(parseEnumToken(item));
356
+ } else if (item !== undefined && item !== null) {
357
+ entries.push({ value: item, description: "" });
358
+ }
359
+ }
360
+ return dedupeEnumEntries(entries);
361
+ }
362
+
363
+ if (typeof raw === "string") {
364
+ const tokens = raw
365
+ .split(/[,,]/)
366
+ .map((item) => item.trim())
367
+ .filter(Boolean);
368
+ for (const token of tokens) {
369
+ entries.push(parseEnumToken(token));
370
+ }
371
+ return dedupeEnumEntries(entries);
372
+ }
373
+
374
+ return [];
375
+ }
376
+
377
+ function extractEnumPrefixFromDescription(rawDescription) {
378
+ if (typeof rawDescription !== "string") {
379
+ return null;
380
+ }
381
+
382
+ const trimmed = sanitizeText(rawDescription);
383
+ if (!trimmed.startsWith("[")) {
384
+ return null;
385
+ }
386
+
387
+ const match = trimmed.match(/^\[([^\]]+)\]\s*(.*)$/);
388
+ if (!match) {
389
+ return null;
390
+ }
391
+
392
+ const enumEntries = parseEnumEntries(match[1]);
393
+ if (enumEntries.length === 0) {
394
+ return null;
395
+ }
396
+
397
+ return {
398
+ entries: enumEntries,
399
+ description: match[2]?.trim() || "",
400
+ };
401
+ }
402
+
403
+ function applyEnumToSchema(schema, enumEntries) {
404
+ if (!schema || !Array.isArray(enumEntries) || enumEntries.length === 0) {
405
+ return;
406
+ }
407
+
408
+ const target =
409
+ schema.type === "array"
410
+ ? (() => {
411
+ if (!schema.items || typeof schema.items !== "object" || Array.isArray(schema.items)) {
412
+ schema.items = { type: "string" };
413
+ }
414
+ return schema.items;
415
+ })()
416
+ : schema;
417
+
418
+ target.enum = enumEntries.map((entry) => entry.value);
419
+ const enumDescriptions = enumEntries.map((entry) => entry.description || "");
420
+ if (enumDescriptions.some((value) => value !== "")) {
421
+ target["x-enumDescriptions"] = enumDescriptions;
422
+ }
423
+ }
424
+
425
+ function parsePrimitive(value) {
426
+ if (typeof value !== "string") {
427
+ return value;
428
+ }
429
+
430
+ const trimmed = value.trim();
431
+ if (trimmed === "") {
432
+ return trimmed;
433
+ }
434
+
435
+ if (
436
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
437
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
438
+ ) {
439
+ return trimmed.slice(1, -1);
440
+ }
441
+
442
+ if (trimmed === "true") {
443
+ return true;
444
+ }
445
+ if (trimmed === "false") {
446
+ return false;
447
+ }
448
+ if (trimmed === "null") {
449
+ return null;
450
+ }
451
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
452
+ return Number(trimmed);
453
+ }
454
+ return trimmed;
455
+ }
456
+
457
+ function schemaFromField(field) {
458
+ const base = schemaFromTypeExpression(field.type);
459
+
460
+ const allowedEntries = parseEnumEntries(field.allowedValues);
461
+ const enumInDescription = extractEnumPrefixFromDescription(field.description);
462
+ const enumEntries =
463
+ allowedEntries.length > 0
464
+ ? allowedEntries
465
+ : enumInDescription?.entries || [];
466
+ applyEnumToSchema(base, enumEntries);
467
+
468
+ if (field.defaultValue !== undefined && field.defaultValue !== null && field.defaultValue !== "") {
469
+ base.default = parsePrimitive(field.defaultValue);
470
+ }
471
+
472
+ const descriptionSource =
473
+ enumInDescription && enumInDescription.description
474
+ ? enumInDescription.description
475
+ : enumInDescription
476
+ ? ""
477
+ : field.description;
478
+ const description = sanitizeText(descriptionSource);
479
+ if (description) {
480
+ base.description = description;
481
+ }
482
+
483
+ return base;
484
+ }
485
+
486
+ function splitFieldPath(fieldName) {
487
+ return normalizeFieldName(fieldName)
488
+ .split(".")
489
+ .map((segment) => segment.trim())
490
+ .filter(Boolean)
491
+ .map((segment) => {
492
+ if (segment.endsWith("[]")) {
493
+ return { key: segment.slice(0, -2), isArray: true };
494
+ }
495
+ return { key: segment, isArray: false };
496
+ });
497
+ }
498
+
499
+ function ensureObjectSchema(target) {
500
+ if (!target.type) {
501
+ target.type = "object";
502
+ }
503
+ if (target.type !== "object") {
504
+ target.type = "object";
505
+ }
506
+ if (!target.properties) {
507
+ target.properties = {};
508
+ }
509
+ return target;
510
+ }
511
+
512
+ function ensureRequiredList(target) {
513
+ if (!target.required) {
514
+ target.required = [];
515
+ }
516
+ return target.required;
517
+ }
518
+
519
+ function markRequired(target, key) {
520
+ const required = ensureRequiredList(target);
521
+ if (!required.includes(key)) {
522
+ required.push(key);
523
+ }
524
+ }
525
+
526
+ function ensureArrayItemsObject(schema) {
527
+ if (!schema.items || typeof schema.items !== "object" || Array.isArray(schema.items)) {
528
+ schema.items = { type: "object", properties: {} };
529
+ }
530
+ ensureObjectSchema(schema.items);
531
+ return schema.items;
532
+ }
533
+
534
+ function mergeObjectSchemas(existing, incoming) {
535
+ const merged = { ...existing, ...incoming, type: "object" };
536
+ const existingProperties =
537
+ existing?.properties && typeof existing.properties === "object"
538
+ ? existing.properties
539
+ : {};
540
+ const incomingProperties =
541
+ incoming?.properties && typeof incoming.properties === "object"
542
+ ? incoming.properties
543
+ : {};
544
+
545
+ const propertyNames = new Set([
546
+ ...Object.keys(existingProperties),
547
+ ...Object.keys(incomingProperties),
548
+ ]);
549
+
550
+ if (propertyNames.size > 0) {
551
+ merged.properties = {};
552
+ for (const name of propertyNames) {
553
+ const left = existingProperties[name];
554
+ const right = incomingProperties[name];
555
+ if (left && right) {
556
+ merged.properties[name] = mergeLeafSchemas(left, right);
557
+ } else {
558
+ merged.properties[name] = safeClone(right || left);
559
+ }
560
+ }
561
+ } else if (!merged.properties) {
562
+ merged.properties = {};
563
+ }
564
+
565
+ const required = new Set([
566
+ ...(Array.isArray(existing?.required) ? existing.required : []),
567
+ ...(Array.isArray(incoming?.required) ? incoming.required : []),
568
+ ]);
569
+ if (required.size > 0) {
570
+ merged.required = Array.from(required);
571
+ }
572
+
573
+ return merged;
574
+ }
575
+
576
+ function mergeLeafSchemas(existing, incoming) {
577
+ if (!existing) {
578
+ return safeClone(incoming);
579
+ }
580
+
581
+ if (existing.type === "array" && incoming.type === "array") {
582
+ const merged = { ...existing, ...incoming };
583
+ const leftItems =
584
+ existing.items && typeof existing.items === "object"
585
+ ? existing.items
586
+ : { type: "object", properties: {} };
587
+ const rightItems =
588
+ incoming.items && typeof incoming.items === "object"
589
+ ? incoming.items
590
+ : { type: "object", properties: {} };
591
+ merged.items = mergeLeafSchemas(leftItems, rightItems);
592
+ return merged;
593
+ }
594
+
595
+ if (existing.type === "array" && incoming.type === "object") {
596
+ const merged = { ...existing };
597
+ const leftItems =
598
+ existing.items && typeof existing.items === "object"
599
+ ? existing.items
600
+ : { type: "object", properties: {} };
601
+ merged.items = mergeLeafSchemas(leftItems, incoming);
602
+ return merged;
603
+ }
604
+
605
+ if (existing.type === "object" && incoming.type === "array") {
606
+ const promoted = { ...incoming };
607
+ const rightItems =
608
+ incoming.items && typeof incoming.items === "object"
609
+ ? incoming.items
610
+ : { type: "object", properties: {} };
611
+ promoted.items = mergeLeafSchemas(existing, rightItems);
612
+ return promoted;
613
+ }
614
+
615
+ if (existing.type === "object" && incoming.type === "object") {
616
+ return mergeObjectSchemas(existing, incoming);
617
+ }
618
+
619
+ return { ...existing, ...incoming };
620
+ }
621
+
622
+ function resolveTraversalSchema(schema, preferArrayTraversal) {
623
+ if (preferArrayTraversal) {
624
+ if (schema.type !== "array") {
625
+ if (schema.type === "object") {
626
+ return { type: "array", items: safeClone(schema) };
627
+ }
628
+ return { type: "array", items: { type: "object", properties: {} } };
629
+ }
630
+ return schema;
631
+ }
632
+ return schema;
633
+ }
634
+
635
+ function assignFieldToSchema(rootSchema, field) {
636
+ const segments = splitFieldPath(field.field);
637
+ if (segments.length === 0) {
638
+ return;
639
+ }
640
+
641
+ let current = ensureObjectSchema(rootSchema);
642
+ const fieldSchema = schemaFromField(field);
643
+ const isRequired = field.optional === false || field.optional === "false";
644
+
645
+ for (let index = 0; index < segments.length; index += 1) {
646
+ const segment = segments[index];
647
+ const isLast = index === segments.length - 1;
648
+
649
+ if (!segment.key) {
650
+ continue;
651
+ }
652
+
653
+ if (isLast) {
654
+ const incoming = segment.isArray
655
+ ? { type: "array", items: fieldSchema }
656
+ : fieldSchema;
657
+ current.properties[segment.key] = mergeLeafSchemas(
658
+ current.properties[segment.key],
659
+ incoming,
660
+ );
661
+ if (isRequired) {
662
+ markRequired(current, segment.key);
663
+ }
664
+ continue;
665
+ }
666
+
667
+ const existing = current.properties[segment.key];
668
+ const base =
669
+ existing && typeof existing === "object" && !Array.isArray(existing)
670
+ ? existing
671
+ : { type: "object", properties: {} };
672
+ const resolved = resolveTraversalSchema(base, segment.isArray);
673
+ current.properties[segment.key] = resolved;
674
+
675
+ if (resolved.type === "array") {
676
+ current = ensureArrayItemsObject(resolved);
677
+ } else {
678
+ current = ensureObjectSchema(resolved);
679
+ }
680
+ }
681
+ }
682
+
683
+ function fieldPathDepth(fieldName) {
684
+ return splitFieldPath(fieldName).length;
685
+ }
686
+
687
+ function fieldsToObjectSchema(fields) {
688
+ const schema = { type: "object", properties: {} };
689
+ const sorted = [...fields].sort((left, right) => {
690
+ const depthDiff = fieldPathDepth(left.field) - fieldPathDepth(right.field);
691
+ if (depthDiff !== 0) {
692
+ return depthDiff;
693
+ }
694
+ return normalizeFieldName(left.field).localeCompare(normalizeFieldName(right.field));
695
+ });
696
+
697
+ for (const field of sorted) {
698
+ if (!field || typeof field !== "object" || !field.field) {
699
+ continue;
700
+ }
701
+ assignFieldToSchema(schema, field);
702
+ }
703
+
704
+ return schema;
705
+ }
706
+
707
+ function extractFieldEntries(fieldGroups) {
708
+ if (!fieldGroups || typeof fieldGroups !== "object") {
709
+ return [];
710
+ }
711
+
712
+ const entries = [];
713
+ for (const [groupName, groupFields] of Object.entries(fieldGroups)) {
714
+ if (!Array.isArray(groupFields)) {
715
+ continue;
716
+ }
717
+ for (const field of groupFields) {
718
+ if (!field || typeof field !== "object") {
719
+ continue;
720
+ }
721
+ entries.push({ ...field, _group: groupName });
722
+ }
723
+ }
724
+
725
+ return entries;
726
+ }
727
+
728
+ function classifyParamLocation(field, method, pathParamNames) {
729
+ const groupName = `${field._group || field.group || ""}`.toLowerCase();
730
+ const normalizedField = normalizePathParamField(field.field);
731
+
732
+ if (pathParamNames.has(normalizedField)) {
733
+ return "path";
734
+ }
735
+ if (groupName.includes("header")) {
736
+ return "header";
737
+ }
738
+ if (groupName.includes("query")) {
739
+ return "query";
740
+ }
741
+ if (groupName.includes("path")) {
742
+ return "path";
743
+ }
744
+ if (groupName.includes("body") || groupName.includes("payload") || groupName.includes("form")) {
745
+ return "body";
746
+ }
747
+ if (BODYLESS_METHODS.has(method)) {
748
+ return "query";
749
+ }
750
+ return "body";
751
+ }
752
+
753
+ function toParameter(field, location) {
754
+ let name = normalizeFieldName(field.field);
755
+ let schema = schemaFromField(field);
756
+
757
+ if (name.endsWith("[]")) {
758
+ name = name.slice(0, -2);
759
+ schema = { type: "array", items: schema };
760
+ }
761
+
762
+ const parameter = {
763
+ name,
764
+ in: location,
765
+ required: location === "path" ? true : !(field.optional === true || field.optional === "true"),
766
+ schema,
767
+ };
768
+
769
+ const description = sanitizeText(field.description);
770
+ if (description) {
771
+ parameter.description = description;
772
+ }
773
+
774
+ return parameter;
775
+ }
776
+
777
+ function parseExamplePayload(content) {
778
+ if (typeof content !== "string") {
779
+ return undefined;
780
+ }
781
+
782
+ const trimmed = content.trim();
783
+ if (!trimmed) {
784
+ return undefined;
785
+ }
786
+
787
+ const candidates = [trimmed];
788
+ const jsonStart = trimmed.search(/[\[{]/);
789
+ if (jsonStart > 0) {
790
+ candidates.push(trimmed.slice(jsonStart));
791
+ }
792
+
793
+ for (const candidate of candidates) {
794
+ try {
795
+ return JSON.parse(candidate);
796
+ } catch {
797
+ // Try next candidate.
798
+ }
799
+ }
800
+
801
+ return undefined;
802
+ }
803
+
804
+ function extractStatusCode(raw, fallback) {
805
+ const source = typeof raw === "string" ? raw : "";
806
+ const match = source.match(/\b([1-5]\d{2})\b/);
807
+ if (match) {
808
+ return match[1];
809
+ }
810
+ return fallback;
811
+ }
812
+
813
+ function addExamplesToResponses(responses, examples, fallbackStatus, fallbackDescription) {
814
+ if (!Array.isArray(examples)) {
815
+ return;
816
+ }
817
+
818
+ for (const example of examples) {
819
+ const payload = parseExamplePayload(example?.content);
820
+ if (payload === undefined) {
821
+ continue;
822
+ }
823
+
824
+ const status = extractStatusCode(example?.title, fallbackStatus);
825
+ if (!responses[status]) {
826
+ responses[status] = { description: fallbackDescription };
827
+ }
828
+
829
+ if (!responses[status].content) {
830
+ responses[status].content = {};
831
+ }
832
+
833
+ if (!responses[status].content["application/json"]) {
834
+ responses[status].content["application/json"] = {};
835
+ }
836
+
837
+ responses[status].content["application/json"].example = payload;
838
+ }
839
+ }
840
+
841
+ function appendFieldResponses(responses, fieldGroups, fallbackStatus, fallbackDescription) {
842
+ if (!fieldGroups || typeof fieldGroups !== "object") {
843
+ return false;
844
+ }
845
+
846
+ let added = false;
847
+ for (const [groupName, fields] of Object.entries(fieldGroups)) {
848
+ if (!Array.isArray(fields)) {
849
+ continue;
850
+ }
851
+
852
+ const status = extractStatusCode(groupName, fallbackStatus);
853
+ const response = {
854
+ description: sanitizeText(groupName) || fallbackDescription,
855
+ };
856
+
857
+ if (fields.length > 0) {
858
+ response.content = {
859
+ "application/json": {
860
+ schema: fieldsToObjectSchema(fields),
861
+ },
862
+ };
863
+ }
864
+
865
+ responses[status] = response;
866
+ added = true;
867
+ }
868
+
869
+ return added;
870
+ }
871
+
872
+ function buildResponses(endpoint) {
873
+ const responses = {};
874
+
875
+ appendFieldResponses(
876
+ responses,
877
+ endpoint?.success?.fields,
878
+ "200",
879
+ "Successful response",
880
+ );
881
+ appendFieldResponses(
882
+ responses,
883
+ endpoint?.error?.fields,
884
+ "400",
885
+ "Error response",
886
+ );
887
+
888
+ addExamplesToResponses(
889
+ responses,
890
+ endpoint?.success?.examples,
891
+ "200",
892
+ "Successful response",
893
+ );
894
+ addExamplesToResponses(
895
+ responses,
896
+ endpoint?.error?.examples,
897
+ "400",
898
+ "Error response",
899
+ );
900
+
901
+ if (Object.keys(responses).length === 0) {
902
+ responses["200"] = { description: "Successful response" };
903
+ }
904
+
905
+ return responses;
906
+ }
907
+
908
+ function dedupeParameters(parameters) {
909
+ const deduped = [];
910
+ const seen = new Set();
911
+ for (const parameter of parameters) {
912
+ const key = `${parameter.in}:${parameter.name}`;
913
+ if (seen.has(key)) {
914
+ continue;
915
+ }
916
+ deduped.push(parameter);
917
+ seen.add(key);
918
+ }
919
+ return deduped;
920
+ }
921
+
922
+ function buildParametersAndRequestBody(endpoint, method, pathname) {
923
+ const pathParamNames = new Set(extractPathParams(pathname));
924
+ const allParamFields = [
925
+ ...extractFieldEntries(endpoint?.header?.fields),
926
+ ...extractFieldEntries(endpoint?.parameter?.fields),
927
+ ];
928
+
929
+ const parameters = [];
930
+ const bodyFields = [];
931
+
932
+ for (const field of allParamFields) {
933
+ const location = classifyParamLocation(field, method, pathParamNames);
934
+ if (location === "body") {
935
+ bodyFields.push(field);
936
+ } else {
937
+ parameters.push(toParameter(field, location));
938
+ }
939
+ }
940
+
941
+ const requestBody = bodyFields.length
942
+ ? {
943
+ required: bodyFields.some(
944
+ (field) => !(field.optional === true || field.optional === "true"),
945
+ ),
946
+ content: {
947
+ "application/json": {
948
+ schema: fieldsToObjectSchema(bodyFields),
949
+ },
950
+ },
951
+ }
952
+ : undefined;
953
+
954
+ const example = parseExamplePayload(endpoint?.parameter?.examples?.[0]?.content);
955
+ if (requestBody && example !== undefined) {
956
+ requestBody.content["application/json"].example = example;
957
+ }
958
+
959
+ return {
960
+ parameters: dedupeParameters(parameters),
961
+ requestBody,
962
+ };
963
+ }
964
+
965
+ function extractEndpoints(docData) {
966
+ if (Array.isArray(docData)) {
967
+ return docData;
968
+ }
969
+
970
+ if (docData && Array.isArray(docData.api)) {
971
+ return docData.api;
972
+ }
973
+
974
+ if (docData && typeof docData === "object") {
975
+ const endpoints = [];
976
+ for (const value of Object.values(docData)) {
977
+ if (!Array.isArray(value)) {
978
+ continue;
979
+ }
980
+ endpoints.push(...value);
981
+ }
982
+ return endpoints;
983
+ }
984
+
985
+ return [];
986
+ }
987
+
988
+ function toOperationId(endpoint, method, pathname, seen) {
989
+ const baseRaw =
990
+ endpoint?.name ||
991
+ `${method}_${pathname.replace(/[{}]/g, "").replace(/[^A-Za-z0-9]+/g, "_")}`;
992
+ const base = baseRaw.replace(/^_+|_+$/g, "") || `${method}_operation`;
993
+
994
+ let candidate = base;
995
+ let suffix = 2;
996
+ while (seen.has(candidate)) {
997
+ candidate = `${base}_${suffix}`;
998
+ suffix += 1;
999
+ }
1000
+ seen.add(candidate);
1001
+ return candidate;
1002
+ }
1003
+
1004
+ function toTags(endpoints) {
1005
+ const tags = new Map();
1006
+ for (const endpoint of endpoints) {
1007
+ const group = sanitizeText(endpoint?.group);
1008
+ if (!group) {
1009
+ continue;
1010
+ }
1011
+ if (!tags.has(group)) {
1012
+ tags.set(group, { name: group });
1013
+ }
1014
+ }
1015
+ return Array.from(tags.values()).sort((a, b) => a.name.localeCompare(b.name));
1016
+ }
1017
+
1018
+ function normalizeServerList(values) {
1019
+ if (!Array.isArray(values)) {
1020
+ return [];
1021
+ }
1022
+
1023
+ const normalized = [];
1024
+ const seen = new Set();
1025
+
1026
+ for (const value of values) {
1027
+ const url = `${value || ""}`.trim();
1028
+ if (!url || seen.has(url)) {
1029
+ continue;
1030
+ }
1031
+ seen.add(url);
1032
+ normalized.push(url);
1033
+ }
1034
+
1035
+ return normalized;
1036
+ }
1037
+
1038
+ function normalizeBasePathValue(value) {
1039
+ if (typeof value !== "string") {
1040
+ return "";
1041
+ }
1042
+
1043
+ let candidate = value.trim();
1044
+ if (!candidate) {
1045
+ return "";
1046
+ }
1047
+
1048
+ try {
1049
+ if (/^[A-Za-z][A-Za-z0-9+\-.]*:\/\//.test(candidate)) {
1050
+ const parsedUrl = new URL(candidate);
1051
+ candidate = parsedUrl.pathname || "";
1052
+ }
1053
+ } catch {
1054
+ // Ignore URL parse errors and keep original candidate.
1055
+ }
1056
+
1057
+ candidate = candidate.split("?")[0].split("#")[0].trim();
1058
+ if (!candidate || candidate === "/") {
1059
+ return "";
1060
+ }
1061
+
1062
+ const withLeadingSlash = candidate.startsWith("/") ? candidate : `/${candidate}`;
1063
+ const normalized = withLeadingSlash.replace(/\/+/g, "/").replace(/\/$/, "");
1064
+ return normalized === "/" ? "" : normalized;
1065
+ }
1066
+
1067
+ function readProjectBasePath(project) {
1068
+ if (!project || typeof project !== "object") {
1069
+ return "";
1070
+ }
1071
+
1072
+ const candidates = [project.baseurl, project.baseUrl, project.url];
1073
+ for (const candidate of candidates) {
1074
+ const basePath = normalizeBasePathValue(candidate);
1075
+ if (basePath) {
1076
+ return basePath;
1077
+ }
1078
+ }
1079
+
1080
+ return "";
1081
+ }
1082
+
1083
+ function joinBasePath(basePath, endpointPath) {
1084
+ const normalizedEndpointPath = toOpenApiPath(endpointPath);
1085
+ if (!basePath) {
1086
+ return normalizedEndpointPath;
1087
+ }
1088
+
1089
+ if (
1090
+ normalizedEndpointPath === basePath ||
1091
+ normalizedEndpointPath.startsWith(`${basePath}/`)
1092
+ ) {
1093
+ return normalizedEndpointPath;
1094
+ }
1095
+
1096
+ if (normalizedEndpointPath === "/") {
1097
+ return basePath;
1098
+ }
1099
+
1100
+ return `${basePath}/${normalizedEndpointPath.replace(/^\/+/, "")}`.replace(/\/+/g, "/");
1101
+ }
1102
+
1103
+ export function apidocDataToOpenApi({
1104
+ docData,
1105
+ project = {},
1106
+ title = "",
1107
+ apiVersion = "",
1108
+ description = "",
1109
+ servers = [],
1110
+ } = {}) {
1111
+ const endpoints = extractEndpoints(docData);
1112
+ const openapi = {
1113
+ openapi: "3.0.3",
1114
+ info: {
1115
+ title: title || sanitizeText(project.title) || sanitizeText(project.name) || "API",
1116
+ version: apiVersion || sanitizeText(project.version) || "1.0.0",
1117
+ },
1118
+ paths: {},
1119
+ };
1120
+
1121
+ const infoDescription =
1122
+ description || sanitizeText(project.description) || sanitizeText(project.header?.description);
1123
+ if (infoDescription) {
1124
+ openapi.info.description = infoDescription;
1125
+ }
1126
+
1127
+ const explicitServers = normalizeServerList(servers);
1128
+ if (explicitServers.length > 0) {
1129
+ openapi.servers = explicitServers.map((url) => ({ url }));
1130
+ }
1131
+
1132
+ const projectBasePath = readProjectBasePath(project);
1133
+ const operationIds = new Set();
1134
+ for (const endpoint of endpoints) {
1135
+ const method = `${endpoint?.type || ""}`.toLowerCase();
1136
+ if (!VALID_METHODS.has(method)) {
1137
+ continue;
1138
+ }
1139
+
1140
+ const pathname = joinBasePath(projectBasePath, endpoint.url);
1141
+ if (!openapi.paths[pathname]) {
1142
+ openapi.paths[pathname] = {};
1143
+ }
1144
+
1145
+ const operation = {
1146
+ operationId: toOperationId(endpoint, method, pathname, operationIds),
1147
+ responses: buildResponses(endpoint),
1148
+ };
1149
+
1150
+ const summary = sanitizeText(endpoint?.title);
1151
+ if (summary) {
1152
+ operation.summary = summary;
1153
+ }
1154
+
1155
+ const opDescription = sanitizeText(endpoint?.description);
1156
+ if (opDescription) {
1157
+ operation.description = opDescription;
1158
+ }
1159
+
1160
+ const group = sanitizeText(endpoint?.group);
1161
+ if (group) {
1162
+ operation.tags = [group];
1163
+ }
1164
+
1165
+ if (endpoint?.deprecated) {
1166
+ operation.deprecated = true;
1167
+ }
1168
+
1169
+ const { parameters, requestBody } = buildParametersAndRequestBody(
1170
+ endpoint,
1171
+ method,
1172
+ pathname,
1173
+ );
1174
+ if (parameters.length > 0) {
1175
+ operation.parameters = parameters;
1176
+ }
1177
+ if (requestBody && !BODYLESS_METHODS.has(method)) {
1178
+ operation.requestBody = requestBody;
1179
+ }
1180
+
1181
+ openapi.paths[pathname][method] = operation;
1182
+ }
1183
+
1184
+ const tags = toTags(endpoints);
1185
+ if (tags.length > 0) {
1186
+ openapi.tags = tags;
1187
+ }
1188
+
1189
+ return openapi;
1190
+ }
1191
+
1192
+ export function serializeOpenApi(document, format = "json", pretty = true) {
1193
+ if (format === "yaml") {
1194
+ return YAML.stringify(document);
1195
+ }
1196
+ return JSON.stringify(document, null, pretty ? 2 : 0);
1197
+ }