swagger-digest 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +108 -2
- package/dist/chunk-UZQM2SHL.js +1412 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +104 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +26 -0
- package/package.json +84 -4
|
@@ -0,0 +1,1412 @@
|
|
|
1
|
+
// src/fetcher.ts
|
|
2
|
+
import { load } from "cheerio";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
var SPEC_FILE_EXTENSION = /\.(?:json|ya?ml)$/i;
|
|
5
|
+
var FALLBACK_PATHS = ["/v3/api-docs", "/swagger/v1/swagger.json", "/swagger.json", "/openapi.json", "/api-docs"];
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function isOpenApiSpec(value) {
|
|
10
|
+
if (!isRecord(value)) return false;
|
|
11
|
+
const hasVersion = typeof value.openapi === "string" || typeof value.swagger === "string";
|
|
12
|
+
const hasInfo = isRecord(value.info) && typeof value.info.title === "string";
|
|
13
|
+
const hasPaths = value.paths === void 0 || isRecord(value.paths);
|
|
14
|
+
return hasVersion && hasInfo && hasPaths;
|
|
15
|
+
}
|
|
16
|
+
function parseSpecText(text, source = "response") {
|
|
17
|
+
const parsers = [() => JSON.parse(text), () => parseYaml(text)];
|
|
18
|
+
const errors = [];
|
|
19
|
+
for (const parser of parsers) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = parser();
|
|
22
|
+
if (isOpenApiSpec(parsed)) return parsed;
|
|
23
|
+
errors.push("document is not a recognizable OpenAPI/Swagger specification");
|
|
24
|
+
} catch (error) {
|
|
25
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`Could not parse OpenAPI spec from ${source}: ${[...new Set(errors)].join("; ")}`);
|
|
29
|
+
}
|
|
30
|
+
function isDirectSpecUrl(url) {
|
|
31
|
+
return SPEC_FILE_EXTENSION.test(url.pathname);
|
|
32
|
+
}
|
|
33
|
+
async function fetchText(url, timeoutMs) {
|
|
34
|
+
const response = await fetch(url, {
|
|
35
|
+
headers: {
|
|
36
|
+
accept: "application/json, application/yaml, text/yaml, text/plain, text/html;q=0.8, */*;q=0.5",
|
|
37
|
+
"user-agent": "swagger-digest/0.1.0"
|
|
38
|
+
},
|
|
39
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
40
|
+
redirect: "follow"
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new Error(`HTTP ${response.status} ${response.statusText} for ${url}`);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
text: await response.text(),
|
|
47
|
+
contentType: response.headers.get("content-type") ?? ""
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function extractScripts(html) {
|
|
51
|
+
const $ = load(html);
|
|
52
|
+
const inline = [];
|
|
53
|
+
const external = [];
|
|
54
|
+
$("script").each((_index, element) => {
|
|
55
|
+
const body = $(element).html();
|
|
56
|
+
if (body?.trim()) inline.push(body);
|
|
57
|
+
const src = $(element).attr("src");
|
|
58
|
+
if (src?.trim()) external.push(src.trim());
|
|
59
|
+
});
|
|
60
|
+
return { inline, external };
|
|
61
|
+
}
|
|
62
|
+
function extractSpecUrlFromScripts(scripts, pageUrl) {
|
|
63
|
+
const urlPattern = /\burl\s*:\s*(["'])(.*?)\1/s;
|
|
64
|
+
for (const script of scripts) {
|
|
65
|
+
const match = urlPattern.exec(script);
|
|
66
|
+
if (!match?.[2]) continue;
|
|
67
|
+
const candidate = match[2].trim();
|
|
68
|
+
if (!candidate || candidate.startsWith("data:")) continue;
|
|
69
|
+
try {
|
|
70
|
+
return new URL(candidate, pageUrl).toString();
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
function extractSpecUrl(scripts, pageUrl) {
|
|
77
|
+
const fromScript = extractSpecUrlFromScripts(scripts, pageUrl);
|
|
78
|
+
if (fromScript) return fromScript;
|
|
79
|
+
const queryUrl = pageUrl.searchParams.get("url");
|
|
80
|
+
if (queryUrl) {
|
|
81
|
+
try {
|
|
82
|
+
return new URL(queryUrl, pageUrl).toString();
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
async function fetchExternalConfigScripts(srcs, pageUrl, timeoutMs, errors) {
|
|
89
|
+
const candidates = srcs.map((src) => {
|
|
90
|
+
try {
|
|
91
|
+
return new URL(src, pageUrl);
|
|
92
|
+
} catch {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
}).filter((url) => Boolean(url && url.origin === pageUrl.origin && /\.m?js(?:$|[?#])/i.test(url.href)));
|
|
96
|
+
const priority = (url) => /swagger[-_]?initializer|swagger[-_]?config|swagger[-_]?ui[-_]?init|openapi[-_]?config|(?:^|[\/_-])config(?:[._/-]|$)/i.test(
|
|
97
|
+
url.pathname
|
|
98
|
+
) ? 0 : 1;
|
|
99
|
+
candidates.sort((a, b) => priority(a) - priority(b));
|
|
100
|
+
const scripts = [];
|
|
101
|
+
for (const candidate of candidates.slice(0, 10)) {
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetchText(candidate.toString(), timeoutMs);
|
|
104
|
+
scripts.push(response.text);
|
|
105
|
+
if (extractSpecUrlFromScripts(scripts, pageUrl) || scripts.some((script) => findObjectLiteralAfterKey(script, "spec"))) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
errors.push(`Swagger UI script (${candidate}): ${error instanceof Error ? error.message : String(error)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return scripts;
|
|
113
|
+
}
|
|
114
|
+
function getFallbackUrls(pageUrl) {
|
|
115
|
+
const relativePaths = [
|
|
116
|
+
"./v3/api-docs",
|
|
117
|
+
"./swagger/v1/swagger.json",
|
|
118
|
+
"./swagger.json",
|
|
119
|
+
"./openapi.json",
|
|
120
|
+
"./api-docs",
|
|
121
|
+
"./doc.json",
|
|
122
|
+
"../v3/api-docs",
|
|
123
|
+
"../swagger/v1/swagger.json",
|
|
124
|
+
"../swagger.json",
|
|
125
|
+
"../openapi.json",
|
|
126
|
+
"../api-docs"
|
|
127
|
+
];
|
|
128
|
+
const absolutePaths = [...relativePaths, ...FALLBACK_PATHS];
|
|
129
|
+
return [...new Set(absolutePaths.map((path2) => new URL(path2, pageUrl).toString()))];
|
|
130
|
+
}
|
|
131
|
+
function formatAttemptErrors(errors) {
|
|
132
|
+
if (errors.length <= 10) return errors.join("\n - ");
|
|
133
|
+
const visible = [...errors.slice(0, 6), `\u2026 ${errors.length - 9} additional attempts omitted \u2026`, ...errors.slice(-3)];
|
|
134
|
+
return visible.join("\n - ");
|
|
135
|
+
}
|
|
136
|
+
function findObjectLiteralAfterKey(source, key) {
|
|
137
|
+
const keyPattern = new RegExp(`\\b${key}\\s*:`);
|
|
138
|
+
const match = keyPattern.exec(source);
|
|
139
|
+
if (!match) return void 0;
|
|
140
|
+
let index = match.index + match[0].length;
|
|
141
|
+
while (/\s/.test(source[index] ?? "")) index += 1;
|
|
142
|
+
if (source[index] !== "{") return void 0;
|
|
143
|
+
const start = index;
|
|
144
|
+
let depth = 0;
|
|
145
|
+
let quote;
|
|
146
|
+
let escaped = false;
|
|
147
|
+
let lineComment = false;
|
|
148
|
+
let blockComment = false;
|
|
149
|
+
for (; index < source.length; index += 1) {
|
|
150
|
+
const char = source[index] ?? "";
|
|
151
|
+
const next = source[index + 1] ?? "";
|
|
152
|
+
if (lineComment) {
|
|
153
|
+
if (char === "\n") lineComment = false;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (blockComment) {
|
|
157
|
+
if (char === "*" && next === "/") {
|
|
158
|
+
blockComment = false;
|
|
159
|
+
index += 1;
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (quote) {
|
|
164
|
+
if (escaped) {
|
|
165
|
+
escaped = false;
|
|
166
|
+
} else if (char === "\\") {
|
|
167
|
+
escaped = true;
|
|
168
|
+
} else if (char === quote) {
|
|
169
|
+
quote = void 0;
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (char === "/" && next === "/") {
|
|
174
|
+
lineComment = true;
|
|
175
|
+
index += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (char === "/" && next === "*") {
|
|
179
|
+
blockComment = true;
|
|
180
|
+
index += 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
184
|
+
quote = char;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (char === "{") depth += 1;
|
|
188
|
+
if (char === "}") {
|
|
189
|
+
depth -= 1;
|
|
190
|
+
if (depth === 0) return source.slice(start, index + 1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return void 0;
|
|
194
|
+
}
|
|
195
|
+
function parseInlineObjectLiteral(literal) {
|
|
196
|
+
const candidates = [
|
|
197
|
+
() => JSON.parse(literal),
|
|
198
|
+
() => parseYaml(literal),
|
|
199
|
+
// Swagger config often uses JS object syntax with trailing commas.
|
|
200
|
+
() => JSON.parse(literal.replace(/,\s*([}\]])/g, "$1"))
|
|
201
|
+
];
|
|
202
|
+
for (const parse of candidates) {
|
|
203
|
+
try {
|
|
204
|
+
const result = parse();
|
|
205
|
+
if (isOpenApiSpec(result)) return result;
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
function extractInlineSpec(scripts) {
|
|
212
|
+
for (const script of scripts) {
|
|
213
|
+
const literal = findObjectLiteralAfterKey(script, "spec");
|
|
214
|
+
if (!literal) continue;
|
|
215
|
+
const spec = parseInlineObjectLiteral(literal);
|
|
216
|
+
if (spec) return spec;
|
|
217
|
+
}
|
|
218
|
+
return void 0;
|
|
219
|
+
}
|
|
220
|
+
async function tryFetchSpec(url, timeoutMs) {
|
|
221
|
+
const response = await fetchText(url, timeoutMs);
|
|
222
|
+
return parseSpecText(response.text, url);
|
|
223
|
+
}
|
|
224
|
+
async function tryBrowserFallback(pageUrl) {
|
|
225
|
+
let playwright;
|
|
226
|
+
try {
|
|
227
|
+
const optionalModuleName = "playwright";
|
|
228
|
+
playwright = await import(optionalModuleName);
|
|
229
|
+
} catch {
|
|
230
|
+
throw new Error(
|
|
231
|
+
"Headless fallback needs optional Playwright. Install it with `npm install playwright` and `npx playwright install chromium`, then rerun."
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const browser = await playwright.chromium.launch({ headless: true });
|
|
235
|
+
try {
|
|
236
|
+
const page = await browser.newPage();
|
|
237
|
+
await page.goto(pageUrl, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
238
|
+
await page.waitForFunction(
|
|
239
|
+
() => {
|
|
240
|
+
const w = window;
|
|
241
|
+
try {
|
|
242
|
+
const candidate = w.ui?.spec?.();
|
|
243
|
+
const document = candidate && typeof candidate.toJSON === "function" ? candidate.toJSON() : candidate;
|
|
244
|
+
return Boolean(document?.openapi || document?.swagger || w.swaggerDoc?.openapi || w.swaggerDoc?.swagger);
|
|
245
|
+
} catch {
|
|
246
|
+
return Boolean(w.swaggerDoc?.openapi || w.swaggerDoc?.swagger);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
void 0,
|
|
250
|
+
{ timeout: 8e3 }
|
|
251
|
+
).catch(() => void 0);
|
|
252
|
+
const extracted = await page.evaluate(() => {
|
|
253
|
+
const w = window;
|
|
254
|
+
try {
|
|
255
|
+
const spec = w.ui?.spec?.();
|
|
256
|
+
if (spec) {
|
|
257
|
+
const json = typeof spec.toJSON === "function" ? spec.toJSON() : spec;
|
|
258
|
+
if (json && typeof json === "object") return json;
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
if (w.swaggerDoc && typeof w.swaggerDoc === "object") return w.swaggerDoc;
|
|
263
|
+
try {
|
|
264
|
+
const state = w.ui?.getSystem?.().specSelectors?.specJson?.();
|
|
265
|
+
if (state) return typeof state.toJS === "function" ? state.toJS() : state;
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
});
|
|
270
|
+
if (!isOpenApiSpec(extracted)) {
|
|
271
|
+
throw new Error("The page loaded in a browser, but no parsed OpenAPI spec was exposed by Swagger UI.");
|
|
272
|
+
}
|
|
273
|
+
return extracted;
|
|
274
|
+
} finally {
|
|
275
|
+
await browser.close();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function fetchOpenApiSpec(input, options = {}) {
|
|
279
|
+
let pageUrl;
|
|
280
|
+
try {
|
|
281
|
+
pageUrl = new URL(input);
|
|
282
|
+
} catch {
|
|
283
|
+
throw new Error(`Invalid URL: ${input}`);
|
|
284
|
+
}
|
|
285
|
+
if (!["http:", "https:"].includes(pageUrl.protocol)) {
|
|
286
|
+
throw new Error("Only HTTP and HTTPS URLs are supported.");
|
|
287
|
+
}
|
|
288
|
+
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
289
|
+
const errors = [];
|
|
290
|
+
if (isDirectSpecUrl(pageUrl)) {
|
|
291
|
+
try {
|
|
292
|
+
const spec = await tryFetchSpec(pageUrl.toString(), timeoutMs);
|
|
293
|
+
return { spec, sourceUrl: pageUrl.toString(), retrievalMethod: "direct-spec" };
|
|
294
|
+
} catch (error) {
|
|
295
|
+
errors.push(`Direct spec: ${error instanceof Error ? error.message : String(error)}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
let html;
|
|
299
|
+
try {
|
|
300
|
+
const response = await fetchText(pageUrl.toString(), timeoutMs);
|
|
301
|
+
try {
|
|
302
|
+
const spec = parseSpecText(response.text, pageUrl.toString());
|
|
303
|
+
return { spec, sourceUrl: pageUrl.toString(), retrievalMethod: "direct-spec" };
|
|
304
|
+
} catch {
|
|
305
|
+
html = response.text;
|
|
306
|
+
}
|
|
307
|
+
} catch (error) {
|
|
308
|
+
errors.push(`Swagger UI page: ${error instanceof Error ? error.message : String(error)}`);
|
|
309
|
+
}
|
|
310
|
+
if (html !== void 0) {
|
|
311
|
+
const pageScripts = extractScripts(html);
|
|
312
|
+
let scripts = pageScripts.inline;
|
|
313
|
+
let specUrl = extractSpecUrl(scripts, pageUrl);
|
|
314
|
+
const inlineSpec = extractInlineSpec(scripts);
|
|
315
|
+
if (inlineSpec) {
|
|
316
|
+
return { spec: inlineSpec, sourceUrl: pageUrl.toString(), retrievalMethod: "inline-spec" };
|
|
317
|
+
}
|
|
318
|
+
if (specUrl) {
|
|
319
|
+
try {
|
|
320
|
+
const spec = await tryFetchSpec(specUrl, timeoutMs);
|
|
321
|
+
return { spec, sourceUrl: specUrl, retrievalMethod: "html-config" };
|
|
322
|
+
} catch (error) {
|
|
323
|
+
errors.push(`Configured spec (${specUrl}): ${error instanceof Error ? error.message : String(error)}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const externalScripts = await fetchExternalConfigScripts(pageScripts.external, pageUrl, timeoutMs, errors);
|
|
327
|
+
scripts = [...scripts, ...externalScripts];
|
|
328
|
+
specUrl = extractSpecUrl(scripts, pageUrl);
|
|
329
|
+
if (specUrl && specUrl !== pageUrl.searchParams.get("url")) {
|
|
330
|
+
try {
|
|
331
|
+
const spec = await tryFetchSpec(specUrl, timeoutMs);
|
|
332
|
+
return { spec, sourceUrl: specUrl, retrievalMethod: "html-config" };
|
|
333
|
+
} catch (error) {
|
|
334
|
+
errors.push(`Configured spec (${specUrl}): ${error instanceof Error ? error.message : String(error)}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (const script of scripts) {
|
|
338
|
+
const literal = findObjectLiteralAfterKey(script, "spec");
|
|
339
|
+
if (!literal) continue;
|
|
340
|
+
const spec = parseInlineObjectLiteral(literal);
|
|
341
|
+
if (spec) return { spec, sourceUrl: pageUrl.toString(), retrievalMethod: "inline-spec" };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const candidate of getFallbackUrls(pageUrl)) {
|
|
345
|
+
try {
|
|
346
|
+
const spec = await tryFetchSpec(candidate, timeoutMs);
|
|
347
|
+
return { spec, sourceUrl: candidate, retrievalMethod: "fallback-path" };
|
|
348
|
+
} catch (error) {
|
|
349
|
+
errors.push(`${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (options.useBrowserFallback !== false) {
|
|
353
|
+
try {
|
|
354
|
+
const spec = await tryBrowserFallback(pageUrl.toString());
|
|
355
|
+
return { spec, sourceUrl: pageUrl.toString(), retrievalMethod: "headless-browser" };
|
|
356
|
+
} catch (error) {
|
|
357
|
+
errors.push(`Headless browser: ${error instanceof Error ? error.message : String(error)}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const details = formatAttemptErrors(errors);
|
|
361
|
+
throw new Error(
|
|
362
|
+
`Unable to extract an OpenAPI definition from ${input}.
|
|
363
|
+
- ${details}
|
|
364
|
+
|
|
365
|
+
Check that the URL is reachable from this terminal and points to a Swagger UI page or OpenAPI spec. If it opens only in a browser, verify that your VPN applies to terminal applications (browser VPN extensions do not automatically route Node.js requests). For JavaScript-only Swagger UI deployments, install Playwright and its Chromium browser; Playwright will also need the same network/VPN access.`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/transformer.ts
|
|
370
|
+
var HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
|
|
371
|
+
var COMMON_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
372
|
+
"400",
|
|
373
|
+
"401",
|
|
374
|
+
"403",
|
|
375
|
+
"404",
|
|
376
|
+
"405",
|
|
377
|
+
"409",
|
|
378
|
+
"415",
|
|
379
|
+
"422",
|
|
380
|
+
"429",
|
|
381
|
+
"500",
|
|
382
|
+
"501",
|
|
383
|
+
"502",
|
|
384
|
+
"503",
|
|
385
|
+
"default"
|
|
386
|
+
]);
|
|
387
|
+
function isObject(value) {
|
|
388
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
389
|
+
}
|
|
390
|
+
function entries(value) {
|
|
391
|
+
return isObject(value) ? Object.entries(value) : [];
|
|
392
|
+
}
|
|
393
|
+
function cleanText(value) {
|
|
394
|
+
return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
|
|
395
|
+
}
|
|
396
|
+
function markdownCell(value) {
|
|
397
|
+
return cleanText(value).replace(/\|/g, "\\|").replace(/`/g, "\\`") || "\u2014";
|
|
398
|
+
}
|
|
399
|
+
function safeSourceUrl(value) {
|
|
400
|
+
try {
|
|
401
|
+
const url = new URL(value);
|
|
402
|
+
url.username = "";
|
|
403
|
+
url.password = "";
|
|
404
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
405
|
+
if (/(token|secret|password|signature|credential|api[-_]?key|authorization|auth)/i.test(key)) {
|
|
406
|
+
url.searchParams.set(key, "[redacted]");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return url.toString();
|
|
410
|
+
} catch {
|
|
411
|
+
return value;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function safeComment(value) {
|
|
415
|
+
return cleanText(value).replace(/\*\//g, "* /").replace(/[\r\n]+/g, " ");
|
|
416
|
+
}
|
|
417
|
+
function schemaMetadata(schemaValue) {
|
|
418
|
+
if (!isObject(schemaValue)) return [];
|
|
419
|
+
const schema = schemaValue;
|
|
420
|
+
const metadata = [];
|
|
421
|
+
if (cleanText(schema.description)) metadata.push(cleanText(schema.description));
|
|
422
|
+
if (cleanText(schema.format)) metadata.push(`format: ${cleanText(schema.format)}`);
|
|
423
|
+
const constraints = [
|
|
424
|
+
["default", "default"],
|
|
425
|
+
["pattern", "pattern"],
|
|
426
|
+
["minLength", "minLength"],
|
|
427
|
+
["maxLength", "maxLength"],
|
|
428
|
+
["minimum", "minimum"],
|
|
429
|
+
["maximum", "maximum"],
|
|
430
|
+
["exclusiveMinimum", "exclusiveMinimum"],
|
|
431
|
+
["exclusiveMaximum", "exclusiveMaximum"],
|
|
432
|
+
["minItems", "minItems"],
|
|
433
|
+
["maxItems", "maxItems"],
|
|
434
|
+
["uniqueItems", "uniqueItems"],
|
|
435
|
+
["multipleOf", "multipleOf"]
|
|
436
|
+
];
|
|
437
|
+
for (const [property, label] of constraints) {
|
|
438
|
+
const value = schema[property];
|
|
439
|
+
if (value === void 0) continue;
|
|
440
|
+
if (property === "default" && (value === null || value === "")) continue;
|
|
441
|
+
metadata.push(
|
|
442
|
+
`${label}: ${property === "default" ? JSON.stringify(value) : typeof value === "string" ? value : JSON.stringify(value)}`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
if (schema.readOnly === true) metadata.push("read-only");
|
|
446
|
+
if (schema.writeOnly === true) metadata.push("write-only");
|
|
447
|
+
if (schema.deprecated === true) metadata.push("deprecated");
|
|
448
|
+
return [...new Set(metadata)];
|
|
449
|
+
}
|
|
450
|
+
function toTypeScriptIdentifier(value) {
|
|
451
|
+
let identifier = value.replace(/[^\p{L}\p{N}_$]+/gu, "_").replace(/^([^\p{L}_$])/u, "_$1");
|
|
452
|
+
if (!identifier) identifier = "Unnamed";
|
|
453
|
+
const reserved = /* @__PURE__ */ new Set([
|
|
454
|
+
"abstract",
|
|
455
|
+
"any",
|
|
456
|
+
"as",
|
|
457
|
+
"asserts",
|
|
458
|
+
"async",
|
|
459
|
+
"await",
|
|
460
|
+
"boolean",
|
|
461
|
+
"break",
|
|
462
|
+
"case",
|
|
463
|
+
"catch",
|
|
464
|
+
"class",
|
|
465
|
+
"const",
|
|
466
|
+
"constructor",
|
|
467
|
+
"continue",
|
|
468
|
+
"debugger",
|
|
469
|
+
"declare",
|
|
470
|
+
"default",
|
|
471
|
+
"delete",
|
|
472
|
+
"do",
|
|
473
|
+
"else",
|
|
474
|
+
"enum",
|
|
475
|
+
"export",
|
|
476
|
+
"extends",
|
|
477
|
+
"false",
|
|
478
|
+
"finally",
|
|
479
|
+
"for",
|
|
480
|
+
"from",
|
|
481
|
+
"function",
|
|
482
|
+
"get",
|
|
483
|
+
"if",
|
|
484
|
+
"implements",
|
|
485
|
+
"import",
|
|
486
|
+
"in",
|
|
487
|
+
"infer",
|
|
488
|
+
"instanceof",
|
|
489
|
+
"interface",
|
|
490
|
+
"is",
|
|
491
|
+
"keyof",
|
|
492
|
+
"let",
|
|
493
|
+
"module",
|
|
494
|
+
"namespace",
|
|
495
|
+
"never",
|
|
496
|
+
"new",
|
|
497
|
+
"null",
|
|
498
|
+
"number",
|
|
499
|
+
"object",
|
|
500
|
+
"of",
|
|
501
|
+
"package",
|
|
502
|
+
"private",
|
|
503
|
+
"protected",
|
|
504
|
+
"public",
|
|
505
|
+
"readonly",
|
|
506
|
+
"require",
|
|
507
|
+
"return",
|
|
508
|
+
"set",
|
|
509
|
+
"static",
|
|
510
|
+
"string",
|
|
511
|
+
"super",
|
|
512
|
+
"switch",
|
|
513
|
+
"symbol",
|
|
514
|
+
"this",
|
|
515
|
+
"throw",
|
|
516
|
+
"true",
|
|
517
|
+
"try",
|
|
518
|
+
"type",
|
|
519
|
+
"typeof",
|
|
520
|
+
"undefined",
|
|
521
|
+
"unique",
|
|
522
|
+
"unknown",
|
|
523
|
+
"var",
|
|
524
|
+
"void",
|
|
525
|
+
"while",
|
|
526
|
+
"with",
|
|
527
|
+
"yield"
|
|
528
|
+
]);
|
|
529
|
+
if (reserved.has(identifier)) identifier = `${identifier}Model`;
|
|
530
|
+
return identifier;
|
|
531
|
+
}
|
|
532
|
+
function propertyKey(value) {
|
|
533
|
+
return /^[$A-Z_a-z][$\w]*$/.test(value) ? value : JSON.stringify(value);
|
|
534
|
+
}
|
|
535
|
+
function refName(ref) {
|
|
536
|
+
const parts = ref.split("/");
|
|
537
|
+
const last = parts[parts.length - 1];
|
|
538
|
+
if (!last) return void 0;
|
|
539
|
+
const decoded = last.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
540
|
+
return toTypeScriptIdentifier(decoded);
|
|
541
|
+
}
|
|
542
|
+
function isSchemaRef(ref) {
|
|
543
|
+
return ref.includes("/components/schemas/") || ref.includes("/definitions/");
|
|
544
|
+
}
|
|
545
|
+
function nullableType(type, schema) {
|
|
546
|
+
const typeList = Array.isArray(schema.type) ? schema.type : [];
|
|
547
|
+
if (schema.nullable === true || typeList.includes("null")) {
|
|
548
|
+
if (!type.split(/\s*\|\s*/).includes("null")) return `${parenthesizeUnion(type)} | null`;
|
|
549
|
+
}
|
|
550
|
+
return type;
|
|
551
|
+
}
|
|
552
|
+
function parenthesizeUnion(type) {
|
|
553
|
+
return type.includes(" | ") ? `(${type})` : type;
|
|
554
|
+
}
|
|
555
|
+
function literalType(value) {
|
|
556
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
557
|
+
if (value === null) return "null";
|
|
558
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
559
|
+
return "unknown";
|
|
560
|
+
}
|
|
561
|
+
function typeExpression(schemaValue, depth = 0) {
|
|
562
|
+
if (depth > 12 || !isObject(schemaValue)) return "unknown";
|
|
563
|
+
const schema = schemaValue;
|
|
564
|
+
if (typeof schema.$ref === "string") {
|
|
565
|
+
return refName(schema.$ref) ?? "unknown";
|
|
566
|
+
}
|
|
567
|
+
if (schema.const !== void 0) return nullableType(literalType(schema.const), schema);
|
|
568
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
569
|
+
return nullableType([...new Set(schema.enum.map(literalType))].join(" | "), schema);
|
|
570
|
+
}
|
|
571
|
+
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
|
|
572
|
+
return nullableType(schema.oneOf.map((item) => typeExpression(item, depth + 1)).join(" | "), schema);
|
|
573
|
+
}
|
|
574
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
|
|
575
|
+
return nullableType(schema.anyOf.map((item) => typeExpression(item, depth + 1)).join(" | "), schema);
|
|
576
|
+
}
|
|
577
|
+
if (Array.isArray(schema.allOf) && schema.allOf.length > 0) {
|
|
578
|
+
const components = schema.allOf.map((item) => typeExpression(item, depth + 1));
|
|
579
|
+
if (schema.properties || schema.additionalProperties !== void 0) {
|
|
580
|
+
components.push(
|
|
581
|
+
typeExpression(
|
|
582
|
+
{
|
|
583
|
+
type: "object",
|
|
584
|
+
properties: schema.properties,
|
|
585
|
+
required: schema.required,
|
|
586
|
+
additionalProperties: schema.additionalProperties
|
|
587
|
+
},
|
|
588
|
+
depth + 1
|
|
589
|
+
)
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
return nullableType(components.join(" & "), schema);
|
|
593
|
+
}
|
|
594
|
+
const rawType = Array.isArray(schema.type) ? schema.type.filter((item) => item !== "null") : schema.type;
|
|
595
|
+
const type = Array.isArray(rawType) ? rawType[0] : rawType;
|
|
596
|
+
if (type === "array" || schema.items) {
|
|
597
|
+
const itemType = typeExpression(schema.items, depth + 1);
|
|
598
|
+
return nullableType(`${itemType.includes(" | ") ? `(${itemType})` : itemType}[]`, schema);
|
|
599
|
+
}
|
|
600
|
+
if (type === "object" || schema.properties || schema.additionalProperties !== void 0) {
|
|
601
|
+
const properties = entries(schema.properties);
|
|
602
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
603
|
+
const lines = properties.map(([name, propertySchema]) => {
|
|
604
|
+
const metadata = schemaMetadata(propertySchema).map(safeComment).filter(Boolean).join("; ");
|
|
605
|
+
const comment = metadata ? `/** ${metadata} */ ` : "";
|
|
606
|
+
return `${comment}${propertyKey(name)}${required.has(name) ? "" : "?"}: ${typeExpression(propertySchema, depth + 1)};`;
|
|
607
|
+
});
|
|
608
|
+
if (schema.additionalProperties === true) lines.push("[key: string]: unknown;");
|
|
609
|
+
else if (isObject(schema.additionalProperties))
|
|
610
|
+
lines.push(`[key: string]: ${typeExpression(schema.additionalProperties, depth + 1)};`);
|
|
611
|
+
return nullableType(lines.length ? `{ ${lines.join(" ")} }` : "Record<string, unknown>", schema);
|
|
612
|
+
}
|
|
613
|
+
let base;
|
|
614
|
+
switch (type) {
|
|
615
|
+
case "string":
|
|
616
|
+
case "file":
|
|
617
|
+
base = cleanText(schema.format) ? `string /* ${safeComment(schema.format)} */` : "string";
|
|
618
|
+
break;
|
|
619
|
+
case "integer":
|
|
620
|
+
case "number":
|
|
621
|
+
base = "number";
|
|
622
|
+
break;
|
|
623
|
+
case "boolean":
|
|
624
|
+
base = "boolean";
|
|
625
|
+
break;
|
|
626
|
+
case "null":
|
|
627
|
+
base = "null";
|
|
628
|
+
break;
|
|
629
|
+
default:
|
|
630
|
+
base = "unknown";
|
|
631
|
+
}
|
|
632
|
+
return nullableType(base, schema);
|
|
633
|
+
}
|
|
634
|
+
function getSchemaDictionary(spec) {
|
|
635
|
+
const schemas = spec.components?.schemas ?? spec.definitions ?? {};
|
|
636
|
+
return isObject(schemas) ? schemas : {};
|
|
637
|
+
}
|
|
638
|
+
function collectSchemaRefs(value, output, root, seen = /* @__PURE__ */ new Set(), followedRefs = /* @__PURE__ */ new Set()) {
|
|
639
|
+
if (!value || typeof value !== "object" || seen.has(value)) return;
|
|
640
|
+
seen.add(value);
|
|
641
|
+
if (Array.isArray(value)) {
|
|
642
|
+
for (const item of value) collectSchemaRefs(item, output, root, seen, followedRefs);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const object = value;
|
|
646
|
+
if (typeof object.$ref === "string") {
|
|
647
|
+
if (isSchemaRef(object.$ref)) {
|
|
648
|
+
const name = refName(object.$ref);
|
|
649
|
+
if (name) output.add(name);
|
|
650
|
+
} else if (root && object.$ref.startsWith("#/") && !followedRefs.has(object.$ref)) {
|
|
651
|
+
followedRefs.add(object.$ref);
|
|
652
|
+
const target = resolvePointer(root, object.$ref);
|
|
653
|
+
if (target !== void 0) collectSchemaRefs(target, output, root, seen, followedRefs);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
for (const [key, child] of Object.entries(object)) {
|
|
657
|
+
if (key !== "example" && key !== "examples" && key !== "externalDocs")
|
|
658
|
+
collectSchemaRefs(child, output, root, seen, followedRefs);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function referencedSchemas(spec) {
|
|
662
|
+
const dictionary = getSchemaDictionary(spec);
|
|
663
|
+
const knownByName = /* @__PURE__ */ new Map();
|
|
664
|
+
for (const [name, schema] of Object.entries(dictionary)) {
|
|
665
|
+
knownByName.set(toTypeScriptIdentifier(name), [name, schema]);
|
|
666
|
+
}
|
|
667
|
+
const refs = /* @__PURE__ */ new Set();
|
|
668
|
+
collectSchemaRefs(spec.paths ?? {}, refs, spec);
|
|
669
|
+
const selected = /* @__PURE__ */ new Set();
|
|
670
|
+
const queue = [...refs];
|
|
671
|
+
while (queue.length) {
|
|
672
|
+
const name = queue.shift();
|
|
673
|
+
if (!name || selected.has(name)) continue;
|
|
674
|
+
selected.add(name);
|
|
675
|
+
const model = knownByName.get(name);
|
|
676
|
+
if (!model) continue;
|
|
677
|
+
const childRefs = /* @__PURE__ */ new Set();
|
|
678
|
+
collectSchemaRefs(model[1], childRefs, spec);
|
|
679
|
+
for (const child of childRefs) if (!selected.has(child)) queue.push(child);
|
|
680
|
+
}
|
|
681
|
+
return [...selected].map((name) => knownByName.get(name)).filter((entry) => Boolean(entry));
|
|
682
|
+
}
|
|
683
|
+
function modelDeclaration(name, schema) {
|
|
684
|
+
const identifier = toTypeScriptIdentifier(name);
|
|
685
|
+
const description = safeComment(schema.description);
|
|
686
|
+
const comment = description ? `/** ${description} */
|
|
687
|
+
` : "";
|
|
688
|
+
if (schema.$ref) return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
|
|
689
|
+
if (Array.isArray(schema.enum) || schema.const !== void 0 || schema.oneOf || schema.anyOf || schema.allOf) {
|
|
690
|
+
return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
|
|
691
|
+
}
|
|
692
|
+
const kind = Array.isArray(schema.type) ? schema.type.find((item) => item !== "null") : schema.type;
|
|
693
|
+
if (kind === "object" || schema.properties || schema.additionalProperties !== void 0) {
|
|
694
|
+
const properties = entries(schema.properties);
|
|
695
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
696
|
+
const fields = properties.map(([field, value]) => {
|
|
697
|
+
const metadata = schemaMetadata(value).map(safeComment).filter(Boolean).join("; ");
|
|
698
|
+
const doc = metadata ? ` /** ${metadata} */
|
|
699
|
+
` : "";
|
|
700
|
+
return `${doc} ${propertyKey(field)}${required.has(field) ? "" : "?"}: ${typeExpression(value)};`;
|
|
701
|
+
});
|
|
702
|
+
if (schema.additionalProperties === true) fields.push(" [key: string]: unknown;");
|
|
703
|
+
else if (isObject(schema.additionalProperties))
|
|
704
|
+
fields.push(` [key: string]: ${typeExpression(schema.additionalProperties)};`);
|
|
705
|
+
return `${comment}export interface ${identifier} {
|
|
706
|
+
${fields.join("\n")}
|
|
707
|
+
}`;
|
|
708
|
+
}
|
|
709
|
+
return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
|
|
710
|
+
}
|
|
711
|
+
function resolvePointer(root, ref) {
|
|
712
|
+
if (!ref.startsWith("#/")) return void 0;
|
|
713
|
+
let current = root;
|
|
714
|
+
for (const rawPart of ref.slice(2).split("/")) {
|
|
715
|
+
const part = rawPart.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
716
|
+
if (!isObject(current)) return void 0;
|
|
717
|
+
current = current[part];
|
|
718
|
+
}
|
|
719
|
+
return current;
|
|
720
|
+
}
|
|
721
|
+
function resolveReferenceObject(spec, value, seen = /* @__PURE__ */ new Set()) {
|
|
722
|
+
if (!isObject(value)) return {};
|
|
723
|
+
if (typeof value.$ref !== "string") return value;
|
|
724
|
+
const ref = value.$ref;
|
|
725
|
+
if (seen.has(ref)) return {};
|
|
726
|
+
const target = resolvePointer(spec, ref);
|
|
727
|
+
if (!isObject(target)) return value;
|
|
728
|
+
const nextSeen = new Set(seen).add(ref);
|
|
729
|
+
return {
|
|
730
|
+
...resolveReferenceObject(spec, target, nextSeen),
|
|
731
|
+
...Object.fromEntries(Object.entries(value).filter(([key]) => key !== "$ref"))
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function getEndpoints(spec) {
|
|
735
|
+
const result = [];
|
|
736
|
+
const paths = isObject(spec.paths) ? spec.paths : {};
|
|
737
|
+
for (const [path2, pathItemValue] of Object.entries(paths)) {
|
|
738
|
+
if (!isObject(pathItemValue)) continue;
|
|
739
|
+
const pathParameters = Array.isArray(pathItemValue.parameters) ? pathItemValue.parameters.map((parameter) => resolveReferenceObject(spec, parameter)) : [];
|
|
740
|
+
for (const method of HTTP_METHODS) {
|
|
741
|
+
const operationValue = pathItemValue[method];
|
|
742
|
+
if (!isObject(operationValue)) continue;
|
|
743
|
+
const firstTag = Array.isArray(operationValue.tags) && operationValue.tags.length ? String(operationValue.tags[0]) : "General";
|
|
744
|
+
result.push({ method, path: path2, operation: operationValue, pathParameters, tag: firstTag });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return result;
|
|
748
|
+
}
|
|
749
|
+
function getServers(spec) {
|
|
750
|
+
if (Array.isArray(spec.servers) && spec.servers.length) {
|
|
751
|
+
return spec.servers.map((server) => cleanText(server?.url)).filter(Boolean);
|
|
752
|
+
}
|
|
753
|
+
if (isObject(spec)) {
|
|
754
|
+
const host = cleanText(spec.host);
|
|
755
|
+
if (host) {
|
|
756
|
+
const schemes = Array.isArray(spec.schemes) && spec.schemes.length ? spec.schemes : ["https"];
|
|
757
|
+
const basePath = cleanText(spec.basePath) || "/";
|
|
758
|
+
return schemes.map(
|
|
759
|
+
(scheme) => `${scheme}://${host}${basePath.startsWith("/") ? basePath : `/${basePath}`}`
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return [];
|
|
764
|
+
}
|
|
765
|
+
function describeSecurityScheme(name, scheme) {
|
|
766
|
+
const type = cleanText(scheme.type).toLowerCase();
|
|
767
|
+
const label = cleanText(scheme.description);
|
|
768
|
+
let details = name;
|
|
769
|
+
if (type === "http" && cleanText(scheme.scheme).toLowerCase() === "bearer")
|
|
770
|
+
details += " (Bearer token via Authorization header)";
|
|
771
|
+
else if (type === "http" && cleanText(scheme.scheme).toLowerCase() === "basic")
|
|
772
|
+
details += " (HTTP Basic via Authorization header)";
|
|
773
|
+
else if (type === "apikey" || type === "apiKey")
|
|
774
|
+
details += ` (API key in ${cleanText(scheme.in) || "header"}: ${cleanText(scheme.name) || "key"})`;
|
|
775
|
+
else if (type === "oauth2") {
|
|
776
|
+
const flows = entries(scheme.flows).map(([flowName]) => flowName).join(", ") || cleanText(scheme.flow);
|
|
777
|
+
details += ` (OAuth2${flows ? `; ${flows}` : ""})`;
|
|
778
|
+
} else if (type === "openidconnect" || type === "openIdConnect") details += " (OpenID Connect)";
|
|
779
|
+
else if (type === "basic") details += " (HTTP Basic)";
|
|
780
|
+
else if (type) details += ` (${type})`;
|
|
781
|
+
if (label) details += ` \u2014 ${label}`;
|
|
782
|
+
return details;
|
|
783
|
+
}
|
|
784
|
+
function getSecuritySchemes(spec) {
|
|
785
|
+
const dictionary = spec.components?.securitySchemes ?? spec.securityDefinitions ?? {};
|
|
786
|
+
return entries(dictionary).filter((entry) => isObject(entry[1]));
|
|
787
|
+
}
|
|
788
|
+
function securityRequirement(spec, operation) {
|
|
789
|
+
const security = operation.security !== void 0 ? operation.security : spec.security;
|
|
790
|
+
if (!Array.isArray(security) || security.length === 0) return "None explicitly required";
|
|
791
|
+
const allSchemes = new Map(getSecuritySchemes(spec));
|
|
792
|
+
return security.map((requirement) => {
|
|
793
|
+
if (!isObject(requirement)) return "";
|
|
794
|
+
const names = Object.entries(requirement).map(([name, scopes]) => {
|
|
795
|
+
const label = allSchemes.has(name) ? name : `${name} (scheme not defined)`;
|
|
796
|
+
return Array.isArray(scopes) && scopes.length ? `${label} [scopes: ${scopes.join(", ")}]` : label;
|
|
797
|
+
});
|
|
798
|
+
return names.length ? names.join(" + ") : "No auth";
|
|
799
|
+
}).filter(Boolean).join(" OR ");
|
|
800
|
+
}
|
|
801
|
+
function mergedParameters(spec, endpoint) {
|
|
802
|
+
const operationParameters = Array.isArray(endpoint.operation.parameters) ? endpoint.operation.parameters.map((parameter) => resolveReferenceObject(spec, parameter)) : [];
|
|
803
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
804
|
+
for (const parameter of [...endpoint.pathParameters, ...operationParameters]) {
|
|
805
|
+
if (parameter.in === "body") continue;
|
|
806
|
+
const key = `${cleanText(parameter.in)}:${cleanText(parameter.name)}`;
|
|
807
|
+
if (key !== ":") byIdentity.set(key, parameter);
|
|
808
|
+
}
|
|
809
|
+
return [...byIdentity.values()];
|
|
810
|
+
}
|
|
811
|
+
function parameterType(parameter) {
|
|
812
|
+
if (isObject(parameter.schema)) return typeExpression(parameter.schema);
|
|
813
|
+
if (parameter.type === "array") return `${typeExpression(parameter.items)}[]`;
|
|
814
|
+
if (parameter.type) return typeExpression({ type: parameter.type, format: parameter.format, enum: parameter.enum });
|
|
815
|
+
return "unknown";
|
|
816
|
+
}
|
|
817
|
+
function parameterDisplayType(parameter) {
|
|
818
|
+
return parameterType(parameter).replace(/\/\*\s*([^*]+?)\s*\*\//g, "($1)");
|
|
819
|
+
}
|
|
820
|
+
function parameterDefault(parameter) {
|
|
821
|
+
if (parameter.default !== void 0) return parameter.default;
|
|
822
|
+
if (isObject(parameter.schema)) return parameter.schema.default;
|
|
823
|
+
return void 0;
|
|
824
|
+
}
|
|
825
|
+
function isMeaningfulDefault(value) {
|
|
826
|
+
return value !== void 0 && value !== null && value !== "";
|
|
827
|
+
}
|
|
828
|
+
function parameterList(parameters) {
|
|
829
|
+
if (!parameters.length) return "_None._";
|
|
830
|
+
return parameters.map((parameter) => {
|
|
831
|
+
const required = parameter.required === true || parameter.in === "path";
|
|
832
|
+
const type = parameterDisplayType(parameter);
|
|
833
|
+
const defaultValue = parameterDefault(parameter);
|
|
834
|
+
const defaultSuffix = isMeaningfulDefault(defaultValue) ? ` (default: ${typeof defaultValue === "string" ? JSON.stringify(defaultValue) : String(defaultValue)})` : "";
|
|
835
|
+
const description = cleanText(parameter.description);
|
|
836
|
+
const details = description ? ` \u2014 ${description}` : "";
|
|
837
|
+
return `- \`${cleanText(parameter.name)}\` (\`${cleanText(parameter.in)}\`${required ? ", required" : ""}) \xB7 \`${type}\`${defaultSuffix}${details}`;
|
|
838
|
+
}).join("\n");
|
|
839
|
+
}
|
|
840
|
+
function chooseMediaType(content) {
|
|
841
|
+
const mediaTypes = Object.keys(content);
|
|
842
|
+
return mediaTypes.find((type) => type.toLowerCase() === "application/json") ?? mediaTypes.find((type) => type.toLowerCase().includes("+json")) ?? mediaTypes[0];
|
|
843
|
+
}
|
|
844
|
+
function hasOwn(value, key) {
|
|
845
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
846
|
+
}
|
|
847
|
+
function firstDeclaredExample(spec, schemaValue, seen = /* @__PURE__ */ new Set()) {
|
|
848
|
+
if (!isObject(schemaValue)) return void 0;
|
|
849
|
+
let schema = schemaValue;
|
|
850
|
+
if (typeof schema.$ref === "string") {
|
|
851
|
+
if (seen.has(schema.$ref)) return void 0;
|
|
852
|
+
const target = resolvePointer(spec, schema.$ref);
|
|
853
|
+
if (!isObject(target)) return void 0;
|
|
854
|
+
const nextSeen = new Set(seen).add(schema.$ref);
|
|
855
|
+
schema = { ...target, ...Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "$ref")) };
|
|
856
|
+
const nested = firstDeclaredExample(spec, schema, nextSeen);
|
|
857
|
+
if (nested !== void 0) return nested;
|
|
858
|
+
}
|
|
859
|
+
if (hasOwn(schema, "example")) return schema.example;
|
|
860
|
+
if (Array.isArray(schema.examples) && schema.examples.length) return schema.examples[0];
|
|
861
|
+
return void 0;
|
|
862
|
+
}
|
|
863
|
+
function generateSchemaExample(spec, schemaValue, depth = 0, seenRefs = /* @__PURE__ */ new Set()) {
|
|
864
|
+
if (depth > 12 || !isObject(schemaValue)) return void 0;
|
|
865
|
+
let schema = schemaValue;
|
|
866
|
+
let nextSeen = seenRefs;
|
|
867
|
+
if (typeof schema.$ref === "string") {
|
|
868
|
+
if (seenRefs.has(schema.$ref)) return null;
|
|
869
|
+
const target = resolvePointer(spec, schema.$ref);
|
|
870
|
+
if (!isObject(target)) return void 0;
|
|
871
|
+
nextSeen = new Set(seenRefs).add(schema.$ref);
|
|
872
|
+
schema = { ...target, ...Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "$ref")) };
|
|
873
|
+
}
|
|
874
|
+
if (hasOwn(schema, "example")) return schema.example;
|
|
875
|
+
if (Array.isArray(schema.examples) && schema.examples.length) return schema.examples[0];
|
|
876
|
+
if (schema.default !== void 0) return schema.default;
|
|
877
|
+
if (schema.const !== void 0) return schema.const;
|
|
878
|
+
if (Array.isArray(schema.enum) && schema.enum.length) return schema.enum[0];
|
|
879
|
+
for (const composition of ["oneOf", "anyOf"]) {
|
|
880
|
+
if (Array.isArray(schema[composition]) && schema[composition].length) {
|
|
881
|
+
return generateSchemaExample(spec, schema[composition][0], depth + 1, nextSeen);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (Array.isArray(schema.allOf) && schema.allOf.length) {
|
|
885
|
+
const values = schema.allOf.map((part) => generateSchemaExample(spec, part, depth + 1, nextSeen)).filter((part) => part !== void 0);
|
|
886
|
+
if (values.every((part) => isObject(part))) return Object.assign({}, ...values);
|
|
887
|
+
return values[0];
|
|
888
|
+
}
|
|
889
|
+
const rawType = Array.isArray(schema.type) ? schema.type.find((item) => item !== "null") : schema.type;
|
|
890
|
+
const type = rawType ?? (schema.properties || schema.additionalProperties !== void 0 ? "object" : void 0);
|
|
891
|
+
if (type === "array" || schema.items || schema.prefixItems) {
|
|
892
|
+
const tupleSchemas = Array.isArray(schema.prefixItems) ? schema.prefixItems : void 0;
|
|
893
|
+
if (tupleSchemas?.length) {
|
|
894
|
+
return tupleSchemas.slice(0, 3).map((item2) => generateSchemaExample(spec, item2, depth + 1, nextSeen));
|
|
895
|
+
}
|
|
896
|
+
const item = generateSchemaExample(spec, schema.items, depth + 1, nextSeen);
|
|
897
|
+
return item === void 0 ? [] : [item];
|
|
898
|
+
}
|
|
899
|
+
if (type === "object") {
|
|
900
|
+
const result = {};
|
|
901
|
+
for (const [name, propertySchema] of entries(schema.properties)) {
|
|
902
|
+
const value = generateSchemaExample(spec, propertySchema, depth + 1, nextSeen);
|
|
903
|
+
if (value !== void 0) result[name] = value;
|
|
904
|
+
}
|
|
905
|
+
if (schema.additionalProperties === true) result.additionalProp1 = "string";
|
|
906
|
+
else if (isObject(schema.additionalProperties)) {
|
|
907
|
+
const value = generateSchemaExample(spec, schema.additionalProperties, depth + 1, nextSeen);
|
|
908
|
+
if (value !== void 0) result.additionalProp1 = value;
|
|
909
|
+
}
|
|
910
|
+
return result;
|
|
911
|
+
}
|
|
912
|
+
if (type === "string") {
|
|
913
|
+
switch (cleanText(schema.format).toLowerCase()) {
|
|
914
|
+
case "uuid":
|
|
915
|
+
return "00000000-0000-4000-8000-000000000001";
|
|
916
|
+
case "date":
|
|
917
|
+
return "2026-01-01";
|
|
918
|
+
case "date-time":
|
|
919
|
+
return "2026-01-01T00:00:00Z";
|
|
920
|
+
case "time":
|
|
921
|
+
return "12:00:00Z";
|
|
922
|
+
case "email":
|
|
923
|
+
return "user@example.com";
|
|
924
|
+
case "uri":
|
|
925
|
+
case "url":
|
|
926
|
+
return "https://example.com/resource";
|
|
927
|
+
case "hostname":
|
|
928
|
+
return "example.com";
|
|
929
|
+
case "ipv4":
|
|
930
|
+
return "192.0.2.1";
|
|
931
|
+
case "ipv6":
|
|
932
|
+
return "2001:db8::1";
|
|
933
|
+
case "byte":
|
|
934
|
+
return "c3RyaW5n";
|
|
935
|
+
default:
|
|
936
|
+
return "string";
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (type === "integer" || type === "number") {
|
|
940
|
+
if (typeof schema.minimum === "number") return schema.minimum;
|
|
941
|
+
if (typeof schema.exclusiveMinimum === "number") return schema.exclusiveMinimum + (type === "integer" ? 1 : 0.1);
|
|
942
|
+
return type === "integer" ? 0 : 0;
|
|
943
|
+
}
|
|
944
|
+
if (type === "boolean") return true;
|
|
945
|
+
if (type === "null") return null;
|
|
946
|
+
if (schema.properties || schema.additionalProperties !== void 0) return {};
|
|
947
|
+
return void 0;
|
|
948
|
+
}
|
|
949
|
+
function payloadExample(spec, mediaValue, schemaValue, legacyExamples) {
|
|
950
|
+
const media = isObject(mediaValue) ? mediaValue : {};
|
|
951
|
+
if (hasOwn(media, "example")) return { value: media.example, origin: "declared" };
|
|
952
|
+
for (const [name, exampleValue] of entries(media.examples)) {
|
|
953
|
+
const example = resolveReferenceObject(spec, exampleValue);
|
|
954
|
+
if (hasOwn(example, "value")) return { value: example.value, origin: "declared", name };
|
|
955
|
+
}
|
|
956
|
+
if (isObject(legacyExamples)) {
|
|
957
|
+
const mediaType = Object.keys(legacyExamples).find((key) => key.toLowerCase() === "application/json") ?? Object.keys(legacyExamples)[0];
|
|
958
|
+
if (mediaType && hasOwn(legacyExamples, mediaType)) {
|
|
959
|
+
return { value: legacyExamples[mediaType], origin: "declared" };
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
const declaredSchemaExample = firstDeclaredExample(spec, schemaValue);
|
|
963
|
+
if (declaredSchemaExample !== void 0) return { value: declaredSchemaExample, origin: "declared" };
|
|
964
|
+
const generated = generateSchemaExample(spec, schemaValue);
|
|
965
|
+
return generated === void 0 ? void 0 : { value: generated, origin: "schema-derived" };
|
|
966
|
+
}
|
|
967
|
+
function requestPayloadDescription(spec, operation) {
|
|
968
|
+
if (isObject(operation.requestBody) || operation.requestBody?.$ref) {
|
|
969
|
+
const body = resolveReferenceObject(spec, operation.requestBody);
|
|
970
|
+
const content = isObject(body.content) ? body.content : {};
|
|
971
|
+
const mediaType = chooseMediaType(content);
|
|
972
|
+
const media = mediaType ? content[mediaType] : void 0;
|
|
973
|
+
const schema = isObject(media?.schema) ? media.schema : void 0;
|
|
974
|
+
return {
|
|
975
|
+
type: schema ? typeExpression(schema) : "unknown",
|
|
976
|
+
mediaType: mediaType || "unknown media type",
|
|
977
|
+
example: media || schema ? payloadExample(spec, media, schema) : void 0,
|
|
978
|
+
required: body.required === true
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
const parameters = Array.isArray(operation.parameters) ? operation.parameters : [];
|
|
982
|
+
const bodyParameter = parameters.find((parameter) => isObject(parameter) && parameter.in === "body");
|
|
983
|
+
if (bodyParameter) {
|
|
984
|
+
const consumes = Array.isArray(operation.consumes) ? operation.consumes : Array.isArray(spec.consumes) ? spec.consumes : ["application/json"];
|
|
985
|
+
return {
|
|
986
|
+
type: typeExpression(bodyParameter.schema),
|
|
987
|
+
mediaType: consumes[0] ?? "application/json",
|
|
988
|
+
example: payloadExample(spec, void 0, bodyParameter.schema),
|
|
989
|
+
required: bodyParameter.required === true
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
return void 0;
|
|
993
|
+
}
|
|
994
|
+
function responseSchema(spec, responseValue) {
|
|
995
|
+
const response = resolveReferenceObject(spec, responseValue);
|
|
996
|
+
if (isObject(response.content)) {
|
|
997
|
+
const mediaType = chooseMediaType(response.content);
|
|
998
|
+
const media = mediaType ? response.content[mediaType] : void 0;
|
|
999
|
+
const schema = isObject(media?.schema) ? media.schema : void 0;
|
|
1000
|
+
return {
|
|
1001
|
+
type: schema ? typeExpression(schema) : void 0,
|
|
1002
|
+
mediaType,
|
|
1003
|
+
example: schema || media ? payloadExample(spec, media, schema, response.examples) : void 0
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
if (isObject(response.schema)) {
|
|
1007
|
+
return {
|
|
1008
|
+
type: typeExpression(response.schema),
|
|
1009
|
+
mediaType: "application/json",
|
|
1010
|
+
example: payloadExample(spec, void 0, response.schema, response.examples)
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
return {};
|
|
1014
|
+
}
|
|
1015
|
+
function formattedExample(value) {
|
|
1016
|
+
try {
|
|
1017
|
+
return JSON.stringify(value, null, 2) ?? "null";
|
|
1018
|
+
} catch {
|
|
1019
|
+
return "null";
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function responseBlock(spec, code, responseValue) {
|
|
1023
|
+
const response = resolveReferenceObject(spec, responseValue);
|
|
1024
|
+
const description = cleanText(response.description);
|
|
1025
|
+
const payload = responseSchema(spec, responseValue);
|
|
1026
|
+
const suffix = payload.type ? ` \u2014 \`${payload.type}\`${payload.mediaType ? ` (${payload.mediaType})` : ""}` : " \u2014 no body schema declared";
|
|
1027
|
+
const lines = [`- **${markdownCell(code)}${description ? ` ${description}` : ""}**${suffix}`];
|
|
1028
|
+
if (payload.example?.origin === "declared") {
|
|
1029
|
+
const label = payload.example.name ? ` \xB7 ${payload.example.name}` : "";
|
|
1030
|
+
lines.push(
|
|
1031
|
+
"",
|
|
1032
|
+
` **Example Value** *(declared in the spec${label})*`,
|
|
1033
|
+
"",
|
|
1034
|
+
" ```json",
|
|
1035
|
+
...formattedExample(payload.example.value).split("\n").map((line) => ` ${line}`),
|
|
1036
|
+
" ```"
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
return lines.join("\n");
|
|
1040
|
+
}
|
|
1041
|
+
var COLLAPSIBLE_ERROR_CODES = /* @__PURE__ */ new Set(["400", "401", "403", "404", "500", "502", "503"]);
|
|
1042
|
+
var GENERIC_ERROR_DESCRIPTIONS = /* @__PURE__ */ new Set([
|
|
1043
|
+
"",
|
|
1044
|
+
"an error occurred",
|
|
1045
|
+
"bad gateway",
|
|
1046
|
+
"bad request",
|
|
1047
|
+
"client error",
|
|
1048
|
+
"error",
|
|
1049
|
+
"failed",
|
|
1050
|
+
"forbidden",
|
|
1051
|
+
"internal error",
|
|
1052
|
+
"internal server error",
|
|
1053
|
+
"invalid request",
|
|
1054
|
+
"not found",
|
|
1055
|
+
"payment required",
|
|
1056
|
+
"request error",
|
|
1057
|
+
"request failed",
|
|
1058
|
+
"server error",
|
|
1059
|
+
"service unavailable",
|
|
1060
|
+
"too many requests",
|
|
1061
|
+
"unauthorized",
|
|
1062
|
+
"unexpected error",
|
|
1063
|
+
"unknown error"
|
|
1064
|
+
]);
|
|
1065
|
+
function isGenericErrorDescription(description) {
|
|
1066
|
+
if (!description) return true;
|
|
1067
|
+
return GENERIC_ERROR_DESCRIPTIONS.has(
|
|
1068
|
+
description.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim()
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
function responseSchemaValue(spec, responseValue) {
|
|
1072
|
+
const response = resolveReferenceObject(spec, responseValue);
|
|
1073
|
+
if (isObject(response.content)) {
|
|
1074
|
+
const mediaType = chooseMediaType(response.content);
|
|
1075
|
+
const media = mediaType ? response.content[mediaType] : void 0;
|
|
1076
|
+
return isObject(media?.schema) ? media.schema : void 0;
|
|
1077
|
+
}
|
|
1078
|
+
if (isObject(response.schema)) return response.schema;
|
|
1079
|
+
return void 0;
|
|
1080
|
+
}
|
|
1081
|
+
function collapsibleErrorEntry(spec, code, responseValue) {
|
|
1082
|
+
if (!COLLAPSIBLE_ERROR_CODES.has(code)) return void 0;
|
|
1083
|
+
const response = resolveReferenceObject(spec, responseValue);
|
|
1084
|
+
if (!isGenericErrorDescription(cleanText(response.description))) return void 0;
|
|
1085
|
+
if (isObject(response.headers) && Object.keys(response.headers).length) return void 0;
|
|
1086
|
+
if (responseSchema(spec, responseValue).example?.origin === "declared") return void 0;
|
|
1087
|
+
const schemaValue = responseSchemaValue(spec, responseValue);
|
|
1088
|
+
if (schemaValue === void 0) return `\`${code}\``;
|
|
1089
|
+
return `\`${code}\` (\`${typeExpression(schemaValue)}\`)`;
|
|
1090
|
+
}
|
|
1091
|
+
function responseSection(spec, operation) {
|
|
1092
|
+
const responses = entries(operation.responses).filter(([, value]) => isObject(value));
|
|
1093
|
+
if (!responses.length) return "_No responses declared._";
|
|
1094
|
+
const success = responses.filter(([code]) => /^2\d\d$/.test(code));
|
|
1095
|
+
success.sort(([a], [b]) => a === "200" ? -1 : b === "200" ? 1 : Number(a) - Number(b));
|
|
1096
|
+
const errors = responses.filter(
|
|
1097
|
+
([code]) => (COMMON_ERROR_CODES.has(code) || /^([45])\d\d$/.test(code)) && !success.some(([successCode]) => successCode === code)
|
|
1098
|
+
);
|
|
1099
|
+
const detailedErrors = [];
|
|
1100
|
+
const collapsedErrors = [];
|
|
1101
|
+
for (const [code, responseValue] of errors) {
|
|
1102
|
+
const entry = collapsibleErrorEntry(spec, code, responseValue);
|
|
1103
|
+
if (entry === void 0) detailedErrors.push(responseBlock(spec, code, responseValue));
|
|
1104
|
+
else collapsedErrors.push(entry);
|
|
1105
|
+
}
|
|
1106
|
+
const primarySuccess = success[0];
|
|
1107
|
+
const lines = primarySuccess ? [responseBlock(spec, primarySuccess[0], primarySuccess[1])] : [];
|
|
1108
|
+
lines.push(...detailedErrors);
|
|
1109
|
+
if (collapsedErrors.length) lines.push(`- **Errors:** ${collapsedErrors.join(", ")}`);
|
|
1110
|
+
if (success.length > 1) {
|
|
1111
|
+
const others = success.slice(1).map(([code, response]) => responseBlock(spec, code, response));
|
|
1112
|
+
lines.push("", "<details><summary>Other successful responses</summary>", "", ...others, "", "</details>");
|
|
1113
|
+
}
|
|
1114
|
+
if (!lines.length) {
|
|
1115
|
+
return responses.map(([code, response]) => responseBlock(spec, code, response)).join("\n\n");
|
|
1116
|
+
}
|
|
1117
|
+
return lines.join("\n");
|
|
1118
|
+
}
|
|
1119
|
+
function shortOperationDescription(operation) {
|
|
1120
|
+
return markdownCell(operation.summary || operation.operationId || operation.description || "\u2014");
|
|
1121
|
+
}
|
|
1122
|
+
function endpointMarkdown(spec, endpoint) {
|
|
1123
|
+
const { operation, method, path: path2 } = endpoint;
|
|
1124
|
+
const heading = `#### \`${method.toUpperCase()} ${path2}\``;
|
|
1125
|
+
const summary = cleanText(operation.summary);
|
|
1126
|
+
const description = cleanText(operation.description);
|
|
1127
|
+
const lines = [heading];
|
|
1128
|
+
if (summary) lines.push("", summary);
|
|
1129
|
+
if (description && description !== summary) lines.push("", description);
|
|
1130
|
+
lines.push("", `**Authentication:** ${securityRequirement(spec, operation)}`);
|
|
1131
|
+
const parameters = mergedParameters(spec, endpoint);
|
|
1132
|
+
const request = requestPayloadDescription(spec, operation);
|
|
1133
|
+
lines.push("", "**Parameters**");
|
|
1134
|
+
lines.push(parameterList(parameters));
|
|
1135
|
+
if (request) {
|
|
1136
|
+
lines.push(
|
|
1137
|
+
"",
|
|
1138
|
+
`**Request body:** \`${request.mediaType}\` \u2192 \`${request.type}\`${request.required ? " (required)" : " (optional)"}`
|
|
1139
|
+
);
|
|
1140
|
+
if (request.example?.origin === "declared") {
|
|
1141
|
+
const label = request.example.name ? ` \xB7 ${request.example.name}` : "";
|
|
1142
|
+
lines.push(
|
|
1143
|
+
"",
|
|
1144
|
+
`**Request Example Value** *(declared in the spec${label})*`,
|
|
1145
|
+
"",
|
|
1146
|
+
"```json",
|
|
1147
|
+
formattedExample(request.example.value),
|
|
1148
|
+
"```"
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
lines.push("", "**Responses**", responseSection(spec, operation));
|
|
1153
|
+
return lines.join("\n");
|
|
1154
|
+
}
|
|
1155
|
+
function authOverview(spec) {
|
|
1156
|
+
const schemes = getSecuritySchemes(spec);
|
|
1157
|
+
if (!schemes.length) return ["- **Authentication:** No security schemes declared."];
|
|
1158
|
+
const globalSecurity = securityRequirement(spec, {});
|
|
1159
|
+
return [
|
|
1160
|
+
`- **Authentication required globally:** ${globalSecurity}`,
|
|
1161
|
+
"- **Available schemes:**",
|
|
1162
|
+
...schemes.map(([name, scheme]) => ` - ${describeSecurityScheme(name, scheme)}`)
|
|
1163
|
+
];
|
|
1164
|
+
}
|
|
1165
|
+
function collectTags(spec, endpoints) {
|
|
1166
|
+
const declared = Array.isArray(spec.tags) ? spec.tags.map((tag) => cleanText(tag?.name)).filter(Boolean) : [];
|
|
1167
|
+
const encountered = [...new Set(endpoints.map((endpoint) => endpoint.tag))];
|
|
1168
|
+
return [
|
|
1169
|
+
...declared.filter((tag) => encountered.includes(tag)),
|
|
1170
|
+
...encountered.filter((tag) => !declared.includes(tag))
|
|
1171
|
+
];
|
|
1172
|
+
}
|
|
1173
|
+
function endpointTypeBase(method, path2, operation) {
|
|
1174
|
+
if (cleanText(operation.operationId)) return toTypeScriptIdentifier(cleanText(operation.operationId));
|
|
1175
|
+
const pathName = path2.replace(/[{}]/g, "").split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1176
|
+
return toTypeScriptIdentifier(`${method.charAt(0).toUpperCase()}${method.slice(1)}${pathName}`);
|
|
1177
|
+
}
|
|
1178
|
+
function normalizeInlinePayloadSchemas(spec) {
|
|
1179
|
+
const document = structuredClone(spec);
|
|
1180
|
+
const isOpenApi3 = typeof document.openapi === "string";
|
|
1181
|
+
const schemaPointer = isOpenApi3 ? "#/components/schemas/" : "#/definitions/";
|
|
1182
|
+
let schemaBucket;
|
|
1183
|
+
if (isOpenApi3) {
|
|
1184
|
+
if (!isObject(document.components)) document.components = {};
|
|
1185
|
+
if (!isObject(document.components.schemas)) document.components.schemas = {};
|
|
1186
|
+
schemaBucket = document.components.schemas;
|
|
1187
|
+
} else {
|
|
1188
|
+
if (!isObject(document.definitions)) document.definitions = {};
|
|
1189
|
+
schemaBucket = document.definitions;
|
|
1190
|
+
}
|
|
1191
|
+
const uniqueName = (suggestion) => {
|
|
1192
|
+
const base = toTypeScriptIdentifier(suggestion);
|
|
1193
|
+
let candidate = base;
|
|
1194
|
+
let suffix = 2;
|
|
1195
|
+
while (Object.prototype.hasOwnProperty.call(schemaBucket, candidate)) {
|
|
1196
|
+
candidate = `${base}${suffix}`;
|
|
1197
|
+
suffix += 1;
|
|
1198
|
+
}
|
|
1199
|
+
return candidate;
|
|
1200
|
+
};
|
|
1201
|
+
const replaceInline = (schema, name) => {
|
|
1202
|
+
if (!isObject(schema) || typeof schema.$ref === "string" || Object.keys(schema).length === 0) return schema;
|
|
1203
|
+
const unique = uniqueName(name);
|
|
1204
|
+
schemaBucket[unique] = schema;
|
|
1205
|
+
return { $ref: `${schemaPointer}${unique.replace(/~/g, "~0").replace(/\//g, "~1")}` };
|
|
1206
|
+
};
|
|
1207
|
+
const reusableBodies = isOpenApi3 ? document.components?.requestBodies : document.parameters;
|
|
1208
|
+
for (const [bodyName, bodyValue] of entries(reusableBodies)) {
|
|
1209
|
+
if (!isObject(bodyValue)) continue;
|
|
1210
|
+
if (isOpenApi3 && isObject(bodyValue.content)) {
|
|
1211
|
+
const mediaType = chooseMediaType(bodyValue.content);
|
|
1212
|
+
const media = mediaType ? bodyValue.content[mediaType] : void 0;
|
|
1213
|
+
if (isObject(media) && isObject(media.schema)) media.schema = replaceInline(media.schema, `${bodyName}Request`);
|
|
1214
|
+
} else if (!isOpenApi3 && bodyValue.in === "body" && isObject(bodyValue.schema)) {
|
|
1215
|
+
bodyValue.schema = replaceInline(bodyValue.schema, `${bodyName}Request`);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
const reusableResponses = isOpenApi3 ? document.components?.responses : document.responses;
|
|
1219
|
+
for (const [responseName, responseValue] of entries(reusableResponses)) {
|
|
1220
|
+
if (!isObject(responseValue)) continue;
|
|
1221
|
+
if (isOpenApi3 && isObject(responseValue.content)) {
|
|
1222
|
+
const mediaType = chooseMediaType(responseValue.content);
|
|
1223
|
+
const media = mediaType ? responseValue.content[mediaType] : void 0;
|
|
1224
|
+
if (isObject(media) && isObject(media.schema))
|
|
1225
|
+
media.schema = replaceInline(media.schema, `${responseName}Response`);
|
|
1226
|
+
} else if (!isOpenApi3 && isObject(responseValue.schema)) {
|
|
1227
|
+
responseValue.schema = replaceInline(responseValue.schema, `${responseName}Response`);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
const paths = isObject(document.paths) ? document.paths : {};
|
|
1231
|
+
for (const [path2, pathItemValue] of Object.entries(paths)) {
|
|
1232
|
+
if (!isObject(pathItemValue)) continue;
|
|
1233
|
+
for (const method of HTTP_METHODS) {
|
|
1234
|
+
const operation = pathItemValue[method];
|
|
1235
|
+
if (!isObject(operation)) continue;
|
|
1236
|
+
const base = endpointTypeBase(method, path2, operation);
|
|
1237
|
+
if (isOpenApi3 && isObject(operation.requestBody) && isObject(operation.requestBody.content)) {
|
|
1238
|
+
const content = operation.requestBody.content;
|
|
1239
|
+
const mediaType = chooseMediaType(content);
|
|
1240
|
+
const media = mediaType ? content[mediaType] : void 0;
|
|
1241
|
+
if (isObject(media) && isObject(media.schema)) {
|
|
1242
|
+
media.schema = replaceInline(media.schema, `${base}Request`);
|
|
1243
|
+
}
|
|
1244
|
+
} else if (!isOpenApi3 && Array.isArray(operation.parameters)) {
|
|
1245
|
+
for (const parameter of operation.parameters) {
|
|
1246
|
+
if (isObject(parameter) && parameter.in === "body" && isObject(parameter.schema)) {
|
|
1247
|
+
parameter.schema = replaceInline(parameter.schema, `${base}Request`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (isObject(operation.responses)) {
|
|
1252
|
+
for (const [code, responseValue] of Object.entries(operation.responses)) {
|
|
1253
|
+
if (!isObject(responseValue) || typeof responseValue.$ref === "string") continue;
|
|
1254
|
+
const suffix = code === "default" ? "Default" : code.replace(/[^a-zA-Z0-9]/g, "");
|
|
1255
|
+
if (isObject(responseValue.content)) {
|
|
1256
|
+
const mediaType = chooseMediaType(responseValue.content);
|
|
1257
|
+
const media = mediaType ? responseValue.content[mediaType] : void 0;
|
|
1258
|
+
if (isObject(media) && isObject(media.schema)) {
|
|
1259
|
+
media.schema = replaceInline(media.schema, `${base}${suffix}Response`);
|
|
1260
|
+
}
|
|
1261
|
+
} else if (!isOpenApi3 && isObject(responseValue.schema)) {
|
|
1262
|
+
responseValue.schema = replaceInline(responseValue.schema, `${base}${suffix}Response`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return document;
|
|
1269
|
+
}
|
|
1270
|
+
function transformOpenApiToMarkdown(spec, sourceUrl) {
|
|
1271
|
+
const normalizedSpec = normalizeInlinePayloadSchemas(spec);
|
|
1272
|
+
spec = normalizedSpec;
|
|
1273
|
+
const info = isObject(spec.info) ? spec.info : {};
|
|
1274
|
+
const title = cleanText(info.title) || "API";
|
|
1275
|
+
const version = cleanText(info.version) || "unspecified";
|
|
1276
|
+
const openApiVersion = cleanText(spec.openapi) || (spec.swagger ? `Swagger ${spec.swagger}` : "OpenAPI");
|
|
1277
|
+
const servers = getServers(spec);
|
|
1278
|
+
const endpoints = getEndpoints(spec);
|
|
1279
|
+
const tags = collectTags(spec, endpoints);
|
|
1280
|
+
const indexRows = endpoints.map(
|
|
1281
|
+
(endpoint) => `| **${endpoint.method.toUpperCase()}** | \`${markdownCell(endpoint.path)}\` | ${shortOperationDescription(endpoint.operation)} | ${markdownCell(Array.isArray(endpoint.operation.tags) ? endpoint.operation.tags.join(", ") : endpoint.tag)} |`
|
|
1282
|
+
);
|
|
1283
|
+
const groupedEndpoints = tags.map((tag) => {
|
|
1284
|
+
const members = endpoints.filter((endpoint) => endpoint.tag === tag);
|
|
1285
|
+
if (!members.length) return "";
|
|
1286
|
+
return [`### ${tag}`, ...members.map((endpoint) => endpointMarkdown(spec, endpoint))].join("\n\n");
|
|
1287
|
+
}).filter(Boolean);
|
|
1288
|
+
const models = referencedSchemas(spec);
|
|
1289
|
+
const modelSection = models.length ? [
|
|
1290
|
+
"## Data types",
|
|
1291
|
+
"",
|
|
1292
|
+
"```typescript",
|
|
1293
|
+
models.map(([name, schema]) => modelDeclaration(name, schema)).join("\n\n"),
|
|
1294
|
+
"```"
|
|
1295
|
+
].join("\n") : "";
|
|
1296
|
+
const description = cleanText(info.description);
|
|
1297
|
+
const sections = [
|
|
1298
|
+
`# API Digest: ${title}`,
|
|
1299
|
+
"",
|
|
1300
|
+
`> Spec: ${openApiVersion} \xB7 API version: ${version}${sourceUrl ? ` \xB7 Source: \`${markdownCell(safeSourceUrl(sourceUrl))}\`` : ""}`,
|
|
1301
|
+
"",
|
|
1302
|
+
"## Overview",
|
|
1303
|
+
...description ? ["", description] : [],
|
|
1304
|
+
"",
|
|
1305
|
+
`- **Base URL(s):** ${servers.length ? servers.map((server) => `\`${markdownCell(server)}\``).join(", ") : "Not declared"}`,
|
|
1306
|
+
...authOverview(spec),
|
|
1307
|
+
"",
|
|
1308
|
+
"## Quick index",
|
|
1309
|
+
"",
|
|
1310
|
+
"| Method | Path | Summary | Tags |",
|
|
1311
|
+
"|---|---|---|---|",
|
|
1312
|
+
...indexRows.length ? indexRows : ["| \u2014 | \u2014 | No operations declared | \u2014 |"],
|
|
1313
|
+
"",
|
|
1314
|
+
"## Endpoints",
|
|
1315
|
+
"",
|
|
1316
|
+
...groupedEndpoints.length ? groupedEndpoints : ["_No endpoints declared._"],
|
|
1317
|
+
...modelSection ? ["", modelSection] : []
|
|
1318
|
+
];
|
|
1319
|
+
return `${sections.join("\n").replace(/\n{3,}/g, "\n\n").trim()}
|
|
1320
|
+
`;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// src/tokens.ts
|
|
1324
|
+
function estimateTokens(text) {
|
|
1325
|
+
const characters = text.length;
|
|
1326
|
+
if (characters === 0) {
|
|
1327
|
+
const zeroModel = (name, window2) => ({
|
|
1328
|
+
name,
|
|
1329
|
+
tokens: 0,
|
|
1330
|
+
contextWindow: window2,
|
|
1331
|
+
contextPercent: 0
|
|
1332
|
+
});
|
|
1333
|
+
return {
|
|
1334
|
+
characters: 0,
|
|
1335
|
+
models: {
|
|
1336
|
+
claude: zeroModel("Claude (Sonnet / Opus)", 2e5),
|
|
1337
|
+
openai: zeroModel("OpenAI (GPT-4o / o-series)", 128e3),
|
|
1338
|
+
gemini: zeroModel("Google Gemini (Pro / Flash)", 1e6),
|
|
1339
|
+
deepseek: zeroModel("DeepSeek (V3 / R1)", 128e3),
|
|
1340
|
+
llama: zeroModel("Meta Llama (3.3 / 4)", 128e3)
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
const wordCount = (text.match(/\b[\w$]+\b/gu) || []).length;
|
|
1345
|
+
const syntaxSymbols = (text.match(/[{}[\]()<>=:;,."`|/-]/g) || []).length;
|
|
1346
|
+
const whitespaces = (text.match(/\s+/g) || []).length;
|
|
1347
|
+
const baseTokens = wordCount * 1.18 + syntaxSymbols * 0.42 + whitespaces * 0.05;
|
|
1348
|
+
const calculate = (name, ratio, contextWindow) => {
|
|
1349
|
+
const tokens = Math.max(1, Math.round(baseTokens * ratio));
|
|
1350
|
+
const contextPercent = Number((tokens / contextWindow * 100).toFixed(1));
|
|
1351
|
+
return { name, tokens, contextWindow, contextPercent };
|
|
1352
|
+
};
|
|
1353
|
+
return {
|
|
1354
|
+
characters,
|
|
1355
|
+
models: {
|
|
1356
|
+
// Claude BPE splits subwords and code tokens slightly finer
|
|
1357
|
+
claude: calculate("Claude (Sonnet / Opus)", 1.08, 2e5),
|
|
1358
|
+
// OpenAI o200k_base has efficient compression on whitespace and symbols
|
|
1359
|
+
openai: calculate("OpenAI (GPT-4o / o-series)", 0.95, 128e3),
|
|
1360
|
+
// Gemini 256k vocab is the most compressed on markdown & code
|
|
1361
|
+
gemini: calculate("Google Gemini (Pro / Flash)", 0.9, 1e6),
|
|
1362
|
+
// DeepSeek 128k tokenizer
|
|
1363
|
+
deepseek: calculate("DeepSeek (V3 / R1)", 0.97, 128e3),
|
|
1364
|
+
// Llama 3/4 128k tokenizer
|
|
1365
|
+
llama: calculate("Meta Llama (3.3 / 4)", 0.98, 128e3)
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// src/writer.ts
|
|
1371
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
1372
|
+
import os from "os";
|
|
1373
|
+
import path from "path";
|
|
1374
|
+
function defaultOutputDirectory() {
|
|
1375
|
+
return path.join(os.homedir(), "Downloads");
|
|
1376
|
+
}
|
|
1377
|
+
function defaultFilename(apiTitle) {
|
|
1378
|
+
const slug = (apiTitle ?? "").normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1379
|
+
return `${slug || "api"}-digest.md`;
|
|
1380
|
+
}
|
|
1381
|
+
function resolveOutputDirectory(directory, cwd = process.cwd()) {
|
|
1382
|
+
const expanded = directory === "~" ? os.homedir() : directory.startsWith(`~${path.sep}`) ? path.join(os.homedir(), directory.slice(2)) : directory;
|
|
1383
|
+
return path.resolve(cwd, expanded);
|
|
1384
|
+
}
|
|
1385
|
+
function normalizeFilename(filename, fallback = "api-digest.md") {
|
|
1386
|
+
const normalized = filename.trim().replace(/\\/g, "/");
|
|
1387
|
+
const basename = path.posix.basename(normalized);
|
|
1388
|
+
if (!basename || basename === "." || basename === "..") return fallback;
|
|
1389
|
+
return basename.toLowerCase().endsWith(".md") ? basename : `${basename}.md`;
|
|
1390
|
+
}
|
|
1391
|
+
async function writeDigest(options) {
|
|
1392
|
+
const directory = resolveOutputDirectory(options.directory);
|
|
1393
|
+
const filename = normalizeFilename(options.filename);
|
|
1394
|
+
const outputPath = path.join(directory, filename);
|
|
1395
|
+
await mkdir(directory, { recursive: true });
|
|
1396
|
+
await writeFile(outputPath, options.markdown, { encoding: "utf8" });
|
|
1397
|
+
return outputPath;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
export {
|
|
1401
|
+
isOpenApiSpec,
|
|
1402
|
+
parseSpecText,
|
|
1403
|
+
fetchOpenApiSpec,
|
|
1404
|
+
toTypeScriptIdentifier,
|
|
1405
|
+
transformOpenApiToMarkdown,
|
|
1406
|
+
estimateTokens,
|
|
1407
|
+
defaultOutputDirectory,
|
|
1408
|
+
defaultFilename,
|
|
1409
|
+
resolveOutputDirectory,
|
|
1410
|
+
normalizeFilename,
|
|
1411
|
+
writeDigest
|
|
1412
|
+
};
|