Repository navigation
Expand file tree
/
Copy pathopenapi.js
More file actions
209 lines (196 loc) · 7.6 KB
/
Copy pathopenapi.js
File metadata and controls
209 lines (196 loc) · 7.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/*
* Reconstruct OpenAPI operation docs from a Mintlify RSC payload.
*
* Mintlify flattens its OpenAPI spec and embeds it in the page payload as a map
* of `"<uuid>": <value>` definitions plus operation objects whose
* parameters/requestBody/responses/schema fields are UUID *references* into
* that map (content-hash dedup). We:
* 1. Build the uuid -> raw-JSON map by balanced-span scanning the payload.
* 2. Find operation objects (those carrying "operationId" + "method").
* 3. Deep-resolve their UUID refs against the map (cycle-guarded).
* 4. Render each to Markdown (method/path, description, params, body,
* responses, curl sample).
*/
const UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/;
const UUID_ONLY = new RegExp('^' + UUID.source + '$');
/* Match a balanced JSON value starting at index i (on { [ " or a scalar).
* Returns [rawString, endIndex] or null. Respects string escapes. */
function matchValue(s, i) {
while (i < s.length && /\s/.test(s[i])) i++;
const c = s[i];
if (c === '"') {
let j = i + 1;
while (j < s.length) {
if (s[j] === '\\') { j += 2; continue; }
if (s[j] === '"') { j++; break; }
j++;
}
return [s.slice(i, j), j];
}
if (c === '{' || c === '[') {
const open = c, close = c === '{' ? '}' : ']';
let depth = 0, j = i, inStr = false;
for (; j < s.length; j++) {
const ch = s[j];
if (inStr) {
if (ch === '\\') { j++; continue; }
if (ch === '"') inStr = false;
continue;
}
if (ch === '"') inStr = true;
else if (ch === open) depth++;
else if (ch === close) { depth--; if (depth === 0) { j++; break; } }
}
return [s.slice(i, j), j];
}
// scalar: number / true / false / null
let j = i;
while (j < s.length && !',}]'.includes(s[j])) j++;
return [s.slice(i, j).trim(), j];
}
/* Build uuid -> parsed-value map from every `"<uuid>": <value>` in the text. */
function buildRefMap(text) {
const map = new Map();
const keyRe = /"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"\s*:/g;
let m;
while ((m = keyRe.exec(text))) {
const id = m[1];
if (map.has(id)) continue;
const colon = text.indexOf(':', m.index + m[0].length - 1);
const got = matchValue(text, colon + 1);
if (!got) continue;
try { map.set(id, JSON.parse(got[0])); } catch { /* skip non-JSON */ }
}
return map;
}
/* Recursively replace UUID string refs with their resolved values. */
function resolve(node, map, seen = new Set(), depth = 0) {
if (depth > 40) return node;
if (typeof node === 'string') {
if (UUID_ONLY.test(node) && map.has(node) && !seen.has(node)) {
return resolve(map.get(node), map, new Set(seen).add(node), depth + 1);
}
return node;
}
if (Array.isArray(node)) return node.map((x) => resolve(x, map, seen, depth + 1));
if (node && typeof node === 'object') {
const out = {};
for (const k of Object.keys(node)) out[k] = resolve(node[k], map, seen, depth + 1);
return out;
}
return node;
}
/* Find operation objects in the payload and return them resolved. */
function extractOperations(text) {
const map = buildRefMap(text);
const ops = [];
const seen = new Set();
// Anchor on "summary": it appears early in every operation object (before the
// code samples, whose literal {} in curl bodies would corrupt the backward
// brace scan) and unlike "operationId" it is always present. We brace-match
// backward to the object start, then forward-match (string-aware) the value.
const re = /"summary"\s*:\s*"/g;
let m;
while ((m = re.exec(text))) {
let depth = 0, start = -1;
for (let j = m.index; j >= 0; j--) {
const ch = text[j];
if (ch === '}') depth++;
else if (ch === '{') { if (depth === 0) { start = j; break; } depth--; }
}
if (start < 0) continue;
const got = matchValue(text, start);
if (!got) continue;
let op;
try { op = JSON.parse(got[0]); } catch { continue; }
if (!op || !op.method || !op.path) continue; // not an operation object
const key = `${op.method} ${op.path}`;
if (seen.has(key)) continue;
seen.add(key);
ops.push(resolve(op, map));
}
return ops;
}
/* ---- Markdown rendering of a resolved operation ---- */
function schemaType(s) {
if (!s || typeof s !== 'object') return typeof s === 'string' ? s : 'any';
if (s.type === 'array' && s.items) return `array<${schemaType(s.items)}>`;
if (s.enum) return `enum(${s.enum.join(', ')})`;
return s.type || (s.properties ? 'object' : (s.format || 'any'));
}
function renderProps(schema, prefix = '', lines = [], depth = 0) {
if (!schema || depth > 6) return lines;
const props = schema.properties || (schema.items && schema.items.properties);
const req = new Set(schema.required || (schema.items && schema.items.required) || []);
if (!props) return lines;
for (const name of Object.keys(props)) {
const p = props[name];
const t = schemaType(p);
const r = req.has(name) ? ' **required**' : '';
const d = (p.description || '').replace(/\s+/g, ' ').trim();
lines.push(`| \`${prefix}${name}\` | ${t} |${r ? ' yes' : ' no'} | ${d} |`);
if (p.properties) renderProps(p, `${prefix}${name}.`, lines, depth + 1);
else if (p.items && p.items.properties) renderProps(p.items, `${prefix}${name}[].`, lines, depth + 1);
}
return lines;
}
function renderOperation(op) {
const out = [];
const method = (op.method || '').toUpperCase();
const path = op.path || '';
const base = (op.baseUrlOptions && op.baseUrlOptions[0]) || '';
out.push(`## ${op.summary || op.title || op.operationId || 'Endpoint'}`);
out.push('');
out.push('```http');
out.push(`${method} ${base}${path}`);
out.push('```');
if (op.description) out.push('\n' + op.description.replace(/\s+/g, ' ').trim());
// Parameters (path/query/header).
const params = Array.isArray(op.parameters) ? op.parameters : [];
if (params.length) {
out.push('\n### Parameters\n');
out.push('| Name | In | Type | Required | Description |');
out.push('| --- | --- | --- | --- | --- |');
for (const p of params) {
if (!p || typeof p !== 'object') continue;
out.push(`| \`${p.name || ''}\` | ${p.in || ''} | ${schemaType(p.schema)} | ${p.required ? 'yes' : 'no'} | ${(p.description || '').replace(/\s+/g, ' ').trim()} |`);
}
}
// Request body schema.
const body = op.requestBody;
const bodySchema = body && body.content &&
(body.content['application/json'] || Object.values(body.content)[0] || {}).schema;
if (bodySchema) {
const rows = renderProps(bodySchema, '', []);
if (rows.length) {
out.push('\n### Request body\n');
out.push('| Field | Type | Required | Description |');
out.push('| --- | --- | --- | --- |');
out.push(...rows);
}
}
// Responses.
if (op.responses && typeof op.responses === 'object') {
out.push('\n### Responses\n');
for (const code of Object.keys(op.responses)) {
const r = op.responses[code] || {};
out.push(`- **${code}** — ${(r.description || '').replace(/\s+/g, ' ').trim()}`);
}
}
// Code sample (curl).
const sample = (op.codeSamples && op.codeSamples[0]) ||
(op['x-code-samples'] && op['x-code-samples'][0]);
if (sample && sample.source) {
out.push(`\n### Example (${sample.lang || 'curl'})\n`);
out.push('```' + (sample.lang || 'bash'));
out.push(sample.source);
out.push('```');
}
return out.join('\n');
}
function operationsMarkdown(text) {
const ops = extractOperations(text);
if (!ops.length) return '';
return ops.map(renderOperation).join('\n\n').trim();
}
module.exports = { extractOperations, operationsMarkdown, buildRefMap, resolve };