@dogsbay/format-openapi 0.2.0-beta.117 → 0.2.0-beta.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core.d.ts +66 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +658 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/parse.d.ts +9 -26
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +9 -596
- package/dist/parse.js.map +1 -1
- package/dist/spec-path.d.ts +34 -0
- package/dist/spec-path.d.ts.map +1 -0
- package/dist/spec-path.js +69 -0
- package/dist/spec-path.js.map +1 -0
- package/package.json +12 -3
package/dist/parse.js
CHANGED
|
@@ -19,20 +19,8 @@
|
|
|
19
19
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
20
20
|
import { readFile } from "node:fs/promises";
|
|
21
21
|
import { resolve, join, isAbsolute } from "node:path";
|
|
22
|
-
import { dereference, upgrade } from "@scalar/openapi-parser";
|
|
23
|
-
import MarkdownIt from "markdown-it";
|
|
24
22
|
import { parse as parseYaml } from "yaml";
|
|
25
|
-
import {
|
|
26
|
-
import { resolveCodeSamples } from "./code-samples.js";
|
|
27
|
-
const HTTP_METHODS = [
|
|
28
|
-
"get",
|
|
29
|
-
"post",
|
|
30
|
-
"put",
|
|
31
|
-
"patch",
|
|
32
|
-
"delete",
|
|
33
|
-
"head",
|
|
34
|
-
"options",
|
|
35
|
-
];
|
|
23
|
+
import { embedSpec, parseSpecText, prepareSpec, specToPages, } from "./core.js";
|
|
36
24
|
/** Extensions a spec file can plausibly have. See the note in detectSpecPath. */
|
|
37
25
|
const SPEC_FILE_EXT = /\.(ya?ml|json)$/i;
|
|
38
26
|
/** Auto-detect spec location given the configured source path. */
|
|
@@ -160,235 +148,19 @@ export async function parseOpenApiSpec(sourcePath, options = {}) {
|
|
|
160
148
|
throw new Error(`OpenAPI spec not found at ${sourcePath}. Looked for openapi.{yaml,yml,json} ` +
|
|
161
149
|
`and spec.{yaml,yml,json} (or expected a direct file path / URL).`);
|
|
162
150
|
}
|
|
163
|
-
|
|
164
|
-
const hrefPrefix = normalisePrefix(options.hrefPrefix);
|
|
165
|
-
const pages = [];
|
|
166
|
-
// Collect tag descriptions (from spec.tags[]) for tag overview pages.
|
|
167
|
-
const tagDescriptions = new Map();
|
|
168
|
-
for (const t of spec.tags || []) {
|
|
169
|
-
if (t?.name)
|
|
170
|
-
tagDescriptions.set(t.name, t.description || "");
|
|
171
|
-
}
|
|
172
|
-
const tagToOps = new Map();
|
|
173
|
-
const untagged = [];
|
|
174
|
-
for (const op of collectOperations(spec)) {
|
|
175
|
-
const { method, path, operation, props } = op;
|
|
176
|
-
const primaryTag = props.tags?.[0];
|
|
177
|
-
const tagSlug = primaryTag ? slugifyTag(primaryTag) : "";
|
|
178
|
-
const opSlug = operationToSlug(method, path, operation.operationId);
|
|
179
|
-
const slug = tagSlug ? `${tagSlug}/${opSlug}` : opSlug;
|
|
180
|
-
const title = operation.summary || `${method.toUpperCase()} ${path}`;
|
|
181
|
-
const tree = [
|
|
182
|
-
{
|
|
183
|
-
type: "endpoint",
|
|
184
|
-
props: props,
|
|
185
|
-
},
|
|
186
|
-
];
|
|
187
|
-
const headings = [{ depth: 1, slug: opSlug, text: title }];
|
|
188
|
-
pages.push({
|
|
189
|
-
slug,
|
|
190
|
-
title,
|
|
191
|
-
tree,
|
|
192
|
-
headings,
|
|
193
|
-
});
|
|
194
|
-
const summary = {
|
|
195
|
-
slug,
|
|
196
|
-
title,
|
|
197
|
-
method,
|
|
198
|
-
path,
|
|
199
|
-
deprecated: operation.deprecated === true,
|
|
200
|
-
};
|
|
201
|
-
if (primaryTag) {
|
|
202
|
-
const arr = tagToOps.get(primaryTag) || [];
|
|
203
|
-
arr.push(summary);
|
|
204
|
-
tagToOps.set(primaryTag, arr);
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
untagged.push(summary);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
// Tag overview pages.
|
|
211
|
-
for (const [tagName, ops] of tagToOps) {
|
|
212
|
-
const tagSlug = slugifyTag(tagName);
|
|
213
|
-
const description = tagDescriptions.get(tagName) || "";
|
|
214
|
-
pages.push(buildTagPage(tagName, tagSlug, description, ops));
|
|
215
|
-
}
|
|
216
|
-
// Root info page.
|
|
217
|
-
const info = spec.info || {};
|
|
218
|
-
pages.push(buildInfoPage(info.title || "API Reference", info.description || "", info.version || ""));
|
|
219
|
-
// Build nav: tag groups in declaration order, with operations as
|
|
220
|
-
// children. Untagged operations land under a synthetic "Other" group
|
|
221
|
-
// so they're discoverable but visually separated.
|
|
222
|
-
const nav = buildNav(spec, pages, tagToOps, untagged, hrefPrefix);
|
|
223
|
-
return { pages, nav };
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Read, parse and dereference a spec. Swagger 2.0 is upgraded to OpenAPI 3
|
|
227
|
-
* first: everything below reads the 3.x shape (`requestBody`, response
|
|
228
|
-
* `content`, `servers`), so a 2.0 spec parsed without the upgrade lost every
|
|
229
|
-
* body parameter and response schema while still producing plausible-looking
|
|
230
|
-
* pages. Canonical's Anbox Cloud API specs are 2.0.
|
|
231
|
-
*/
|
|
232
|
-
async function loadSpec(specPath) {
|
|
233
|
-
const raw = await readSpecText(specPath);
|
|
234
|
-
let parsed;
|
|
235
|
-
try {
|
|
236
|
-
parsed = JSON.parse(raw);
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
parsed = parseYaml(raw);
|
|
240
|
-
}
|
|
241
|
-
if (typeof parsed?.swagger === "string" && parsed.swagger.startsWith("2")) {
|
|
242
|
-
parsed = upgrade(parsed).specification;
|
|
243
|
-
}
|
|
244
|
-
const { schema: spec } = await dereference(parsed);
|
|
245
|
-
if (!spec) {
|
|
246
|
-
throw new Error(`Failed to dereference OpenAPI spec at ${specPath}`);
|
|
247
|
-
}
|
|
248
|
-
return spec;
|
|
151
|
+
return specToPages(await loadSpec(specPath), options);
|
|
249
152
|
}
|
|
250
|
-
/**
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
const out = [];
|
|
254
|
-
for (const [path, pathItem] of Object.entries(spec.paths || {})) {
|
|
255
|
-
if (!pathItem)
|
|
256
|
-
continue;
|
|
257
|
-
for (const method of HTTP_METHODS) {
|
|
258
|
-
const operation = pathItem[method];
|
|
259
|
-
if (!operation)
|
|
260
|
-
continue;
|
|
261
|
-
const opTags = Array.isArray(operation.tags) ? operation.tags : [];
|
|
262
|
-
const props = {
|
|
263
|
-
method,
|
|
264
|
-
path,
|
|
265
|
-
baseUrl,
|
|
266
|
-
summary: operation.summary || undefined,
|
|
267
|
-
description: operation.description || undefined,
|
|
268
|
-
operationId: operation.operationId || undefined,
|
|
269
|
-
tags: opTags.length > 0 ? opTags : undefined,
|
|
270
|
-
deprecated: operation.deprecated === true ? true : undefined,
|
|
271
|
-
parameters: extractParameters(operation, pathItem),
|
|
272
|
-
...extractRequestBody(operation),
|
|
273
|
-
responses: extractResponses(operation),
|
|
274
|
-
codeSamples: resolveCodeSamples(operation, method, path, baseUrl),
|
|
275
|
-
};
|
|
276
|
-
out.push({ method, path, operation, props });
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
return out;
|
|
153
|
+
/** Read a spec file (or URL) and prepare it (core.ts `prepareSpec`). */
|
|
154
|
+
export async function loadSpec(specPath) {
|
|
155
|
+
return prepareSpec(parseSpecText(await readSpecText(specPath), parseYaml), specPath);
|
|
280
156
|
}
|
|
281
157
|
/**
|
|
282
|
-
* A whole spec as the BODY of one page
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* each holding its operations as `endpoint` nodes in document order.
|
|
286
|
-
*
|
|
287
|
-
* `parseOpenApiSpec` is the other shape — a page per operation — for a spec
|
|
288
|
-
* that IS the source. This one is for a spec a prose page points at.
|
|
158
|
+
* A whole spec as the BODY of one page (core.ts `embedSpec`), read from a
|
|
159
|
+
* file. Each call parses the spec; a build that embeds one spec on several
|
|
160
|
+
* pages uses `createSpecCache` and `embedSpec` instead.
|
|
289
161
|
*/
|
|
290
162
|
export async function openApiEmbed(specPath, options = {}) {
|
|
291
|
-
|
|
292
|
-
const level = Math.min(6, Math.max(2, options.level ?? 2));
|
|
293
|
-
const tree = [];
|
|
294
|
-
const headings = [];
|
|
295
|
-
const used = new Set(options.usedIds ?? []);
|
|
296
|
-
// A description's own headings go under the section that holds it, with
|
|
297
|
-
// ids, and into the outline (a `# Title` was a second H1).
|
|
298
|
-
const describe = (text, minLevel) => {
|
|
299
|
-
const d = descriptionHtml(text, minLevel, used);
|
|
300
|
-
headings.push(...d.headings);
|
|
301
|
-
tree.push({ type: "prose", html: d.html });
|
|
302
|
-
};
|
|
303
|
-
const info = spec.info || {};
|
|
304
|
-
if (info.version) {
|
|
305
|
-
tree.push({
|
|
306
|
-
type: "prose",
|
|
307
|
-
html: `<p class="text-sm text-muted-foreground">Version ${escapeHtml(String(info.version))}</p>`,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
if (typeof info.description === "string" && info.description.trim()) {
|
|
311
|
-
describe(info.description, level);
|
|
312
|
-
}
|
|
313
|
-
const tagDescriptions = new Map();
|
|
314
|
-
for (const t of spec.tags || []) {
|
|
315
|
-
if (t?.name)
|
|
316
|
-
tagDescriptions.set(t.name, t.description || "");
|
|
317
|
-
}
|
|
318
|
-
const groups = new Map();
|
|
319
|
-
for (const t of tagDescriptions.keys())
|
|
320
|
-
groups.set(t, []);
|
|
321
|
-
const untagged = [];
|
|
322
|
-
const operations = collectOperations(spec);
|
|
323
|
-
for (const { props } of operations) {
|
|
324
|
-
const node = { type: "endpoint", props: props };
|
|
325
|
-
const tag = props.tags?.[0];
|
|
326
|
-
if (!tag) {
|
|
327
|
-
untagged.push(node);
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
if (!groups.has(tag))
|
|
331
|
-
groups.set(tag, []);
|
|
332
|
-
groups.get(tag).push(node);
|
|
333
|
-
}
|
|
334
|
-
if (untagged.length)
|
|
335
|
-
groups.set("\u0000other", untagged);
|
|
336
|
-
for (const [tag, nodes] of groups) {
|
|
337
|
-
if (!nodes.length)
|
|
338
|
-
continue;
|
|
339
|
-
const text = tag === "\u0000other" ? "Other" : tag;
|
|
340
|
-
const id = uniqueId(slugifyTag(text) || "section", used);
|
|
341
|
-
tree.push({
|
|
342
|
-
type: "heading",
|
|
343
|
-
props: { level, id },
|
|
344
|
-
inline: [{ type: "text", text: text }],
|
|
345
|
-
});
|
|
346
|
-
headings.push({ depth: level, slug: id, text });
|
|
347
|
-
const description = tagDescriptions.get(tag);
|
|
348
|
-
if (description)
|
|
349
|
-
describe(description, level + 1);
|
|
350
|
-
tree.push(...nodes);
|
|
351
|
-
}
|
|
352
|
-
return { tree, headings, operations: operations.length };
|
|
353
|
-
}
|
|
354
|
-
function uniqueId(base, used) {
|
|
355
|
-
let id = base;
|
|
356
|
-
for (let n = 2; used.has(id); n++)
|
|
357
|
-
id = `${base}-${n}`;
|
|
358
|
-
used.add(id);
|
|
359
|
-
return id;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* A description (CommonMark) → HTML with its headings shifted so the
|
|
363
|
-
* highest is `minLevel`, each given a unique id, and listed for the TOC.
|
|
364
|
-
* Raw HTML stays off (see `md`).
|
|
365
|
-
*/
|
|
366
|
-
function descriptionHtml(text, minLevel, used) {
|
|
367
|
-
const tokens = md.parse(text, {});
|
|
368
|
-
const levels = tokens.filter((t) => t.type === "heading_open").map((t) => Number(t.tag.slice(1)));
|
|
369
|
-
const headings = [];
|
|
370
|
-
if (levels.length) {
|
|
371
|
-
const shift = Math.max(0, minLevel - Math.min(...levels));
|
|
372
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
373
|
-
const t = tokens[i];
|
|
374
|
-
if (t.type !== "heading_open")
|
|
375
|
-
continue;
|
|
376
|
-
const depth = Math.min(6, Number(t.tag.slice(1)) + shift);
|
|
377
|
-
const inline = tokens[i + 1];
|
|
378
|
-
const label = (inline?.children ?? [])
|
|
379
|
-
.filter((c) => c.type === "text" || c.type === "code_inline")
|
|
380
|
-
.map((c) => c.content)
|
|
381
|
-
.join("")
|
|
382
|
-
.trim();
|
|
383
|
-
const id = uniqueId(slugifyTag(label) || "section", used);
|
|
384
|
-
t.tag = `h${depth}`;
|
|
385
|
-
t.attrSet("id", id);
|
|
386
|
-
if (tokens[i + 2]?.type === "heading_close")
|
|
387
|
-
tokens[i + 2].tag = `h${depth}`;
|
|
388
|
-
headings.push({ depth, slug: id, text: label });
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
return { html: md.renderer.render(tokens, md.options, {}).trim(), headings };
|
|
163
|
+
return embedSpec(await loadSpec(specPath), options);
|
|
392
164
|
}
|
|
393
165
|
async function readSpecText(specPath) {
|
|
394
166
|
if (specPath.startsWith("http://") || specPath.startsWith("https://")) {
|
|
@@ -402,363 +174,4 @@ async function readSpecText(specPath) {
|
|
|
402
174
|
const abs = isAbsolute(specPath) ? specPath : resolve(specPath);
|
|
403
175
|
return await readFile(abs, "utf-8");
|
|
404
176
|
}
|
|
405
|
-
function normalisePrefix(p) {
|
|
406
|
-
if (!p)
|
|
407
|
-
return "";
|
|
408
|
-
return p.replace(/\/+$/, "");
|
|
409
|
-
}
|
|
410
|
-
/** The one non-null type of a schema: OpenAPI 3.1 (and an upgraded 2.0
|
|
411
|
-
* spec) writes a nullable object as `type: [object, "null"]`. */
|
|
412
|
-
function primaryType(schema) {
|
|
413
|
-
if (!schema)
|
|
414
|
-
return undefined;
|
|
415
|
-
if (!Array.isArray(schema.type))
|
|
416
|
-
return schema.type;
|
|
417
|
-
const types = schema.type.filter((t) => t !== "null");
|
|
418
|
-
return types.length === 1 ? types[0] : undefined;
|
|
419
|
-
}
|
|
420
|
-
function schemaToType(schema, depth = 0) {
|
|
421
|
-
if (!schema)
|
|
422
|
-
return "any";
|
|
423
|
-
// An array of itself (a cycle once dereferenced) has no finite name.
|
|
424
|
-
if (depth > SCHEMA_MAX_DEPTH)
|
|
425
|
-
return "any";
|
|
426
|
-
// OpenAPI 3.1 (and a Swagger 2.0 spec upgraded to it) writes nullable as
|
|
427
|
-
// a type list: `type: [string, "null"]`.
|
|
428
|
-
if (Array.isArray(schema.type)) {
|
|
429
|
-
const types = schema.type.filter((t) => t !== "null");
|
|
430
|
-
if (types.length === 1)
|
|
431
|
-
return schemaToType({ ...schema, type: types[0] }, depth + 1);
|
|
432
|
-
return types.length ? types.join(" | ") : "null";
|
|
433
|
-
}
|
|
434
|
-
if (schema.type === "array") {
|
|
435
|
-
const itemType = schema.items ? schemaToType(schema.items, depth + 1) : "any";
|
|
436
|
-
return `${itemType}[]`;
|
|
437
|
-
}
|
|
438
|
-
return schema.type || "any";
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Bounds on walking one schema. A dereferenced spec is a GRAPH: a
|
|
442
|
-
* self-referencing schema is a cycle (the walk overflowed the stack), and
|
|
443
|
-
* eight properties that each reference the level below grow 8× per level
|
|
444
|
-
* (a 2.2 KB spec made 18 MB of tree at depth 6). Past a bound the walk stops
|
|
445
|
-
* and the property shows without children.
|
|
446
|
-
*/
|
|
447
|
-
const SCHEMA_MAX_DEPTH = 10;
|
|
448
|
-
const SCHEMA_MAX_NODES = 2000;
|
|
449
|
-
const newWalk = () => ({ nodes: SCHEMA_MAX_NODES, depth: 0, ancestors: new Set() });
|
|
450
|
-
/** Run `f` one level into `schema`, or return `empty` past a bound. */
|
|
451
|
-
function descend(walk, schema, empty, f) {
|
|
452
|
-
if (!schema || typeof schema !== "object")
|
|
453
|
-
return f();
|
|
454
|
-
if (walk.depth >= SCHEMA_MAX_DEPTH || walk.nodes <= 0 || walk.ancestors.has(schema))
|
|
455
|
-
return empty;
|
|
456
|
-
walk.depth++;
|
|
457
|
-
walk.ancestors.add(schema);
|
|
458
|
-
try {
|
|
459
|
-
return f();
|
|
460
|
-
}
|
|
461
|
-
finally {
|
|
462
|
-
walk.depth--;
|
|
463
|
-
walk.ancestors.delete(schema);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
function extractSchemaProperties(schema, requiredFields, walk = newWalk()) {
|
|
467
|
-
if (!schema?.properties)
|
|
468
|
-
return [];
|
|
469
|
-
return descend(walk, schema, [], () => propertiesOf(schema, requiredFields, walk));
|
|
470
|
-
}
|
|
471
|
-
function propertiesOf(schema, requiredFields, walk) {
|
|
472
|
-
const required = requiredFields || schema.required || [];
|
|
473
|
-
const out = [];
|
|
474
|
-
for (const [name, prop] of Object.entries(schema.properties)) {
|
|
475
|
-
if (walk.nodes-- <= 0)
|
|
476
|
-
break;
|
|
477
|
-
out.push(propertyOf(name, prop ?? {}, required, walk));
|
|
478
|
-
}
|
|
479
|
-
return out;
|
|
480
|
-
}
|
|
481
|
-
function propertyOf(name, prop, required, walk) {
|
|
482
|
-
const result = {
|
|
483
|
-
name,
|
|
484
|
-
type: schemaToType(prop),
|
|
485
|
-
required: required.includes(name),
|
|
486
|
-
};
|
|
487
|
-
if (prop.description)
|
|
488
|
-
result.description = prop.description;
|
|
489
|
-
if (prop.nullable)
|
|
490
|
-
result.nullable = true;
|
|
491
|
-
if (prop.format)
|
|
492
|
-
result.format = prop.format;
|
|
493
|
-
if (prop.enum)
|
|
494
|
-
result.enum = prop.enum;
|
|
495
|
-
if (prop.default !== undefined)
|
|
496
|
-
result.default = prop.default;
|
|
497
|
-
if (prop.example !== undefined)
|
|
498
|
-
result.example = prop.example;
|
|
499
|
-
if (primaryType(prop) === "object" && prop.properties) {
|
|
500
|
-
result.properties = extractSchemaProperties(prop, undefined, walk);
|
|
501
|
-
}
|
|
502
|
-
if (primaryType(prop) === "array" && prop.items) {
|
|
503
|
-
if (primaryType(prop.items) === "object" && prop.items.properties) {
|
|
504
|
-
result.items = extractSchemaProperties(prop.items, undefined, walk);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
return result;
|
|
508
|
-
}
|
|
509
|
-
function extractParameters(operation, pathItem) {
|
|
510
|
-
const params = [];
|
|
511
|
-
const allParams = [
|
|
512
|
-
...(pathItem.parameters || []),
|
|
513
|
-
...(operation.parameters || []),
|
|
514
|
-
];
|
|
515
|
-
// Operation-level wins over path-level on (name, in) collision.
|
|
516
|
-
const seen = new Map();
|
|
517
|
-
for (const p of allParams) {
|
|
518
|
-
if (!p?.name || !p?.in)
|
|
519
|
-
continue;
|
|
520
|
-
seen.set(`${p.name}:${p.in}`, p);
|
|
521
|
-
}
|
|
522
|
-
for (const p of seen.values()) {
|
|
523
|
-
const param = {
|
|
524
|
-
name: p.name,
|
|
525
|
-
in: p.in,
|
|
526
|
-
type: schemaToType(p.schema),
|
|
527
|
-
required: p.required || false,
|
|
528
|
-
};
|
|
529
|
-
if (p.description)
|
|
530
|
-
param.description = p.description;
|
|
531
|
-
if (p.schema?.default !== undefined) {
|
|
532
|
-
param.default = String(p.schema.default);
|
|
533
|
-
}
|
|
534
|
-
if (p.schema?.enum)
|
|
535
|
-
param.enum = p.schema.enum;
|
|
536
|
-
if (p.deprecated)
|
|
537
|
-
param.deprecated = true;
|
|
538
|
-
params.push(param);
|
|
539
|
-
}
|
|
540
|
-
return params;
|
|
541
|
-
}
|
|
542
|
-
function extractRequestBody(operation) {
|
|
543
|
-
const body = operation.requestBody;
|
|
544
|
-
if (!body?.content)
|
|
545
|
-
return {};
|
|
546
|
-
const mediaType = body.content["application/json"] ||
|
|
547
|
-
Object.values(body.content)[0];
|
|
548
|
-
if (!mediaType?.schema)
|
|
549
|
-
return {};
|
|
550
|
-
return {
|
|
551
|
-
requestBody: extractSchemaProperties(mediaType.schema),
|
|
552
|
-
requestBodyExample: generateExampleFromSchema(mediaType.schema),
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
function extractResponses(operation) {
|
|
556
|
-
if (!operation.responses)
|
|
557
|
-
return [];
|
|
558
|
-
return Object.entries(operation.responses)
|
|
559
|
-
.filter(([status]) => status !== "default")
|
|
560
|
-
.map(([status, resp]) => {
|
|
561
|
-
const result = {
|
|
562
|
-
status,
|
|
563
|
-
description: resp.description,
|
|
564
|
-
};
|
|
565
|
-
if (resp.content) {
|
|
566
|
-
const contentTypeKey = resp.content["application/json"]
|
|
567
|
-
? "application/json"
|
|
568
|
-
: Object.keys(resp.content)[0];
|
|
569
|
-
const mediaType = resp.content[contentTypeKey];
|
|
570
|
-
if (contentTypeKey)
|
|
571
|
-
result.contentType = contentTypeKey;
|
|
572
|
-
if (mediaType?.schema) {
|
|
573
|
-
const rawSchema = mediaType.schema;
|
|
574
|
-
const schema = primaryType(rawSchema) === "array" && rawSchema.items
|
|
575
|
-
? rawSchema.items
|
|
576
|
-
: rawSchema;
|
|
577
|
-
result.schema = extractSchemaProperties(schema);
|
|
578
|
-
result.example = generateExampleFromSchema(rawSchema);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
return result;
|
|
582
|
-
})
|
|
583
|
-
.sort((a, b) => a.status.localeCompare(b.status));
|
|
584
|
-
}
|
|
585
|
-
function generateExampleFromSchema(schema, walk = newWalk()) {
|
|
586
|
-
if (!schema)
|
|
587
|
-
return undefined;
|
|
588
|
-
if (walk.nodes-- <= 0)
|
|
589
|
-
return null;
|
|
590
|
-
return descend(walk, schema, null, () => exampleOf(schema, walk));
|
|
591
|
-
}
|
|
592
|
-
function exampleOf(schema, walk) {
|
|
593
|
-
if (schema.example !== undefined)
|
|
594
|
-
return schema.example;
|
|
595
|
-
switch (primaryType(schema)) {
|
|
596
|
-
case "object": {
|
|
597
|
-
if (!schema.properties)
|
|
598
|
-
return {};
|
|
599
|
-
const obj = {};
|
|
600
|
-
for (const [name, prop] of Object.entries(schema.properties)) {
|
|
601
|
-
obj[name] = generateExampleFromSchema(prop, walk);
|
|
602
|
-
}
|
|
603
|
-
return obj;
|
|
604
|
-
}
|
|
605
|
-
case "array":
|
|
606
|
-
return schema.items ? [generateExampleFromSchema(schema.items, walk)] : [];
|
|
607
|
-
case "string": {
|
|
608
|
-
if (schema.enum)
|
|
609
|
-
return schema.enum[0];
|
|
610
|
-
if (schema.format === "date-time")
|
|
611
|
-
return "2024-01-01T00:00:00Z";
|
|
612
|
-
if (schema.format === "email")
|
|
613
|
-
return "user@example.com";
|
|
614
|
-
if (schema.format === "uri" || schema.format === "url") {
|
|
615
|
-
return "https://example.com";
|
|
616
|
-
}
|
|
617
|
-
if (schema.format === "uuid") {
|
|
618
|
-
return "550e8400-e29b-41d4-a716-446655440000";
|
|
619
|
-
}
|
|
620
|
-
if (schema.default !== undefined)
|
|
621
|
-
return schema.default;
|
|
622
|
-
return "string";
|
|
623
|
-
}
|
|
624
|
-
case "integer":
|
|
625
|
-
return schema.example ?? schema.default ?? 0;
|
|
626
|
-
case "number":
|
|
627
|
-
return schema.example ?? schema.default ?? 0.0;
|
|
628
|
-
case "boolean":
|
|
629
|
-
return schema.example ?? schema.default ?? true;
|
|
630
|
-
default:
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
function buildTagPage(tagName, tagSlug, description, ops) {
|
|
635
|
-
const tree = [
|
|
636
|
-
{
|
|
637
|
-
type: "heading",
|
|
638
|
-
props: { level: 1, id: tagSlug },
|
|
639
|
-
inline: [{ type: "text", text: tagName }],
|
|
640
|
-
},
|
|
641
|
-
];
|
|
642
|
-
const used = new Set([tagSlug]);
|
|
643
|
-
const described = description ? descriptionHtml(description, 2, used) : null;
|
|
644
|
-
if (described)
|
|
645
|
-
tree.push({ type: "prose", html: described.html });
|
|
646
|
-
const items = ops.map((op) => ({
|
|
647
|
-
type: "list-item",
|
|
648
|
-
children: [
|
|
649
|
-
{
|
|
650
|
-
type: "prose",
|
|
651
|
-
html: `<a href="${escapeAttr(op.slug)}"><code>${op.method.toUpperCase()}</code> ${escapeHtml(op.path)}</a>${op.deprecated ? ' <span class="text-muted-foreground">(deprecated)</span>' : ""}`,
|
|
652
|
-
},
|
|
653
|
-
],
|
|
654
|
-
}));
|
|
655
|
-
tree.push({ type: "unordered-list", children: items });
|
|
656
|
-
return {
|
|
657
|
-
slug: tagSlug,
|
|
658
|
-
title: tagName,
|
|
659
|
-
tree,
|
|
660
|
-
headings: [{ depth: 1, slug: tagSlug, text: tagName }, ...(described?.headings ?? [])],
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
function buildInfoPage(title, description, version) {
|
|
664
|
-
const tree = [
|
|
665
|
-
{
|
|
666
|
-
type: "heading",
|
|
667
|
-
props: { level: 1, id: "api-reference" },
|
|
668
|
-
inline: [{ type: "text", text: title }],
|
|
669
|
-
},
|
|
670
|
-
];
|
|
671
|
-
if (version) {
|
|
672
|
-
tree.push({
|
|
673
|
-
type: "prose",
|
|
674
|
-
html: `<p class="text-sm text-muted-foreground">Version ${escapeHtml(version)}</p>`,
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
const described = description ? descriptionHtml(description, 2, new Set(["api-reference"])) : null;
|
|
678
|
-
if (described)
|
|
679
|
-
tree.push({ type: "prose", html: described.html });
|
|
680
|
-
return {
|
|
681
|
-
slug: "index",
|
|
682
|
-
title,
|
|
683
|
-
tree,
|
|
684
|
-
headings: [{ depth: 1, slug: "api-reference", text: title }, ...(described?.headings ?? [])],
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
function buildNav(spec, pages, tagToOps, untagged, hrefPrefix) {
|
|
688
|
-
const nav = [];
|
|
689
|
-
const root = {
|
|
690
|
-
label: (spec.info && spec.info.title) || "API Reference",
|
|
691
|
-
href: hrefHref(hrefPrefix, "index"),
|
|
692
|
-
};
|
|
693
|
-
nav.push(root);
|
|
694
|
-
// Use spec.tags[] order when present so authors control nav order.
|
|
695
|
-
const declaredTagOrder = Array.isArray(spec.tags)
|
|
696
|
-
? spec.tags.map((t) => t?.name).filter(Boolean)
|
|
697
|
-
: [];
|
|
698
|
-
const seenTags = new Set();
|
|
699
|
-
const tagOrder = [];
|
|
700
|
-
for (const t of declaredTagOrder) {
|
|
701
|
-
if (tagToOps.has(t)) {
|
|
702
|
-
tagOrder.push(t);
|
|
703
|
-
seenTags.add(t);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
for (const t of tagToOps.keys()) {
|
|
707
|
-
if (!seenTags.has(t))
|
|
708
|
-
tagOrder.push(t);
|
|
709
|
-
}
|
|
710
|
-
for (const tagName of tagOrder) {
|
|
711
|
-
const ops = tagToOps.get(tagName) || [];
|
|
712
|
-
const tagSlug = slugifyTag(tagName);
|
|
713
|
-
nav.push({
|
|
714
|
-
label: tagName,
|
|
715
|
-
href: hrefHref(hrefPrefix, tagSlug),
|
|
716
|
-
children: ops
|
|
717
|
-
.slice()
|
|
718
|
-
.sort(byMethodThenPath)
|
|
719
|
-
.map((op) => ({
|
|
720
|
-
label: op.title,
|
|
721
|
-
href: hrefHref(hrefPrefix, op.slug),
|
|
722
|
-
})),
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
if (untagged.length > 0) {
|
|
726
|
-
nav.push({
|
|
727
|
-
label: "Other",
|
|
728
|
-
children: untagged
|
|
729
|
-
.slice()
|
|
730
|
-
.sort(byMethodThenPath)
|
|
731
|
-
.map((op) => ({
|
|
732
|
-
label: op.title,
|
|
733
|
-
href: hrefHref(hrefPrefix, op.slug),
|
|
734
|
-
})),
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
return nav;
|
|
738
|
-
}
|
|
739
|
-
function byMethodThenPath(a, b) {
|
|
740
|
-
const m = a.method.localeCompare(b.method);
|
|
741
|
-
if (m !== 0)
|
|
742
|
-
return m;
|
|
743
|
-
return a.path.localeCompare(b.path);
|
|
744
|
-
}
|
|
745
|
-
function hrefHref(prefix, slug) {
|
|
746
|
-
const p = prefix ? `${prefix}/` : "/";
|
|
747
|
-
return slug === "index" ? p : `${p}${slug}`;
|
|
748
|
-
}
|
|
749
|
-
function escapeHtml(s) {
|
|
750
|
-
return s
|
|
751
|
-
.replace(/&/g, "&")
|
|
752
|
-
.replace(/</g, "<")
|
|
753
|
-
.replace(/>/g, ">");
|
|
754
|
-
}
|
|
755
|
-
function escapeAttr(s) {
|
|
756
|
-
return s.replace(/&/g, "&").replace(/"/g, """);
|
|
757
|
-
}
|
|
758
|
-
/**
|
|
759
|
-
* A spec description (CommonMark, per OpenAPI) → HTML, raw HTML off: a `<`
|
|
760
|
-
* in the spec is escaped, and markdown-it refuses `javascript:` links.
|
|
761
|
-
* Plain-text paragraphs showed a spec's `## Authentication` literally.
|
|
762
|
-
*/
|
|
763
|
-
const md = new MarkdownIt({ html: false, linkify: false });
|
|
764
177
|
//# sourceMappingURL=parse.js.map
|