rapidoc 12.0.0 → 12.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -25
- package/dist/rapidoc-flow-min.js +580 -0
- package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
- package/dist/rapidoc-min.js +541 -1309
- package/dist/rapidoc-min.js.LICENSE.txt +381 -0
- package/package.json +21 -18
- package/src/README.md +150 -0
- package/src/components/README.md +91 -0
- package/src/components/api-request.ts +43 -0
- package/src/components/api-response.ts +9 -4
- package/src/components/schema-base-element.ts +18 -4
- package/src/components/schema-table.ts +58 -13
- package/src/components/schema-tree.ts +7 -0
- package/src/components/tag-input.ts +5 -9
- package/src/flow.js +3 -0
- package/src/rapi-flow.ts +1178 -0
- package/src/rapidoc-mini.ts +27 -6
- package/src/rapidoc.ts +108 -62
- package/src/styles/README.md +96 -0
- package/src/styles/css-bundle.js +2 -2
- package/src/styles/endpoint-styles.js +6 -1
- package/src/styles/flow-styles.js +248 -0
- package/src/styles/nav-styles.js +39 -15
- package/src/styles/schema-styles.js +13 -5
- package/src/styles/theme-tokens.js +2 -2
- package/src/templates/README.md +89 -0
- package/src/templates/api-response-template.js +14 -10
- package/src/templates/callback-template.js +1 -1
- package/src/templates/downloads-template.js +12 -27
- package/src/templates/endpoint-template.js +15 -24
- package/src/templates/expanded-endpoint-template.js +8 -4
- package/src/templates/focused-endpoint-template.js +4 -3
- package/src/templates/navbar-template.js +20 -11
- package/src/templates/request-body-template.js +29 -7
- package/src/templates/request-params-template.js +15 -4
- package/src/templates/security-scheme-template.js +11 -2
- package/src/templates/server-template.js +3 -2
- package/src/utils/README.md +146 -0
- package/src/utils/arazzo/README.md +119 -0
- package/src/utils/arazzo/components.js +15 -0
- package/src/utils/arazzo/graph.js +269 -0
- package/src/utils/arazzo/index.js +52 -0
- package/src/utils/arazzo/operations.js +106 -0
- package/src/utils/arazzo/parse.js +196 -0
- package/src/utils/arazzo/refs.js +40 -0
- package/src/utils/arazzo/svg.js +707 -0
- package/src/utils/curl-utils.js +5 -2
- package/src/utils/highlighter.js +3 -0
- package/src/utils/mcp/README.md +155 -0
- package/src/utils/mcp/common.js +45 -346
- package/src/utils/mcp/manifest.js +382 -0
- package/src/utils/mcp/project/server.ts +53 -0
- package/src/utils/mcp/project.js +88 -0
- package/src/utils/mcp/runtime.js +289 -0
- package/src/utils/mcp/schema.js +98 -0
- package/src/utils/mcp/workflow-runtime.js +374 -0
- package/src/utils/mcp/workflows.js +240 -0
- package/src/utils/mcp/zip.js +102 -0
- package/src/utils/mcp-generator.js +29 -125
- package/src/utils/mock-interceptor.js +134 -85
- package/src/utils/request-utils.js +61 -0
- package/src/utils/schema-utils.js +33 -6
- package/src/utils/slugger.js +5 -7
- package/src/utils/spec-parser.js +190 -164
- package/src/utils/url-utils.js +36 -1
- package/dist/rapidoc-static-min.js +0 -13542
- package/src/static/index.d.ts +0 -149
- package/src/static/index.js +0 -62
- package/src/static/navigation.js +0 -497
- package/src/static/render.js +0 -490
- package/src/static/serializer.js +0 -216
- package/src/utils/mcp/generators/csharp.js +0 -242
- package/src/utils/mcp/generators/go.js +0 -298
- package/src/utils/mcp/generators/java.js +0 -330
- package/src/utils/mcp/generators/python.js +0 -248
- package/src/utils/mcp/generators/rust.js +0 -281
- package/src/utils/mcp/generators/typescript.js +0 -290
|
@@ -157,6 +157,67 @@ export function buildUrlEncodedBody(fields = [], dynamicJson = "") {
|
|
|
157
157
|
return params;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
const isComplexSchema = (schema) =>
|
|
161
|
+
!!schema &&
|
|
162
|
+
(schema.type === "object" ||
|
|
163
|
+
(Array.isArray(schema.type) && schema.type.includes("object")) ||
|
|
164
|
+
(!schema.type && !!schema.properties));
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Content-Type of a multipart/form-data part, following OpenAPI's "Special Considerations for multipart Content":
|
|
168
|
+
* the encoding object's contentType when the spec gives one (the first of a comma-separated list), application/json
|
|
169
|
+
* for an object or an array of objects, and "" otherwise (text/plain, or a file's own type).
|
|
170
|
+
*
|
|
171
|
+
* @param {object} schema - Schema of the form field
|
|
172
|
+
* @param {object} [encoding] - The media type's encoding entry for that field
|
|
173
|
+
* @returns {string}
|
|
174
|
+
*/
|
|
175
|
+
export function multipartPartContentType(schema, encoding) {
|
|
176
|
+
const declared =
|
|
177
|
+
typeof encoding?.contentType === "string" ? encoding.contentType.split(",")[0].trim() : "";
|
|
178
|
+
if (declared) {
|
|
179
|
+
return declared;
|
|
180
|
+
}
|
|
181
|
+
if (isComplexSchema(schema) || (schema?.type === "array" && isComplexSchema(schema.items))) {
|
|
182
|
+
return "application/json";
|
|
183
|
+
}
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Quotes and line breaks in part names and file names are percent-encoded, as browsers do for FormData
|
|
188
|
+
const escapePartName = (name) =>
|
|
189
|
+
String(name).replace(/"/g, "%22").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Serializes FormData into a multipart/form-data body in which parts carry a Content-Type.
|
|
193
|
+
* A browser only sends a Content-Type for a part that is a file, and a Blob part always gets filename="blob",
|
|
194
|
+
* which servers read as an upload; so parts with a type in partContentTypes are written by hand.
|
|
195
|
+
*
|
|
196
|
+
* @param {FormData} formData
|
|
197
|
+
* @param {Object<string, string>} partContentTypes - Content-Type per field name, for fields that are not files
|
|
198
|
+
* @param {string} [boundary]
|
|
199
|
+
* @returns {{ body: Blob, contentType: string }}
|
|
200
|
+
*/
|
|
201
|
+
export function serializeMultipartBody(
|
|
202
|
+
formData,
|
|
203
|
+
partContentTypes = {},
|
|
204
|
+
boundary = `----RapiDocFormBoundary${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`,
|
|
205
|
+
) {
|
|
206
|
+
const chunks = [];
|
|
207
|
+
for (const [name, value] of formData.entries()) {
|
|
208
|
+
const isFile = typeof value !== "string";
|
|
209
|
+
let head = `--${boundary}\r\nContent-Disposition: form-data; name="${escapePartName(name)}"`;
|
|
210
|
+
if (isFile) {
|
|
211
|
+
head += `; filename="${escapePartName(value.name ?? "blob")}"\r\nContent-Type: ${value.type || partContentTypes[name] || "application/octet-stream"}`;
|
|
212
|
+
} else if (partContentTypes[name]) {
|
|
213
|
+
head += `\r\nContent-Type: ${partContentTypes[name]}`;
|
|
214
|
+
}
|
|
215
|
+
chunks.push(`${head}\r\n\r\n`, isFile ? value : value.replace(/\r?\n/g, "\r\n"), "\r\n");
|
|
216
|
+
}
|
|
217
|
+
chunks.push(`--${boundary}--\r\n`);
|
|
218
|
+
return { body: new Blob(chunks), contentType: `multipart/form-data; boundary=${boundary}` };
|
|
219
|
+
}
|
|
220
|
+
|
|
160
221
|
/**
|
|
161
222
|
* Builds a FormData instance from structured field entries.
|
|
162
223
|
*
|
|
@@ -129,6 +129,23 @@ export function isBinaryFileField(schema) {
|
|
|
129
129
|
);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// Keywords that only annotate a schema, without telling anything about the type of the value
|
|
133
|
+
const ANNOTATION_KEYWORDS = new Set([
|
|
134
|
+
"title",
|
|
135
|
+
"description",
|
|
136
|
+
"example",
|
|
137
|
+
"examples",
|
|
138
|
+
"default",
|
|
139
|
+
"deprecated",
|
|
140
|
+
"readOnly",
|
|
141
|
+
"writeOnly",
|
|
142
|
+
"externalDocs",
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
function isAnnotationOnly(schema) {
|
|
146
|
+
return Object.keys(schema).every((key) => ANNOTATION_KEYWORDS.has(key) || key.startsWith("x-"));
|
|
147
|
+
}
|
|
148
|
+
|
|
132
149
|
/* Generates an schema object containing type and constraint info */
|
|
133
150
|
export function getTypeInfo(schema) {
|
|
134
151
|
if (!schema) {
|
|
@@ -170,7 +187,7 @@ export function getTypeInfo(schema) {
|
|
|
170
187
|
}
|
|
171
188
|
});
|
|
172
189
|
dataType = subTypes.length > 0 ? subTypes.join("┃") : "{missing-type-info}";
|
|
173
|
-
} else if (
|
|
190
|
+
} else if (isAnnotationOnly(schema)) {
|
|
174
191
|
dataType = "any";
|
|
175
192
|
} else {
|
|
176
193
|
dataType = "{missing-type-info}";
|
|
@@ -217,9 +234,7 @@ export function getTypeInfo(schema) {
|
|
|
217
234
|
html: "",
|
|
218
235
|
};
|
|
219
236
|
|
|
220
|
-
if (info.type === "{
|
|
221
|
-
info.description = schema.$ref.substring(schema.$ref.lastIndexOf("/") + 1);
|
|
222
|
-
} else if (info.type === "{missing-type-info}" || info.type === "any") {
|
|
237
|
+
if (info.type === "{missing-type-info}" || info.type === "any") {
|
|
223
238
|
info.description = info.description || "";
|
|
224
239
|
}
|
|
225
240
|
// Set Allowed Values
|
|
@@ -1316,8 +1331,8 @@ function createPrimitiveAST(schema, name, isRequired) {
|
|
|
1316
1331
|
title: (schema.title || "").trim(),
|
|
1317
1332
|
required: isRequired,
|
|
1318
1333
|
deprecated: schema.deprecated || !!typeInfo.deprecated,
|
|
1319
|
-
readOnly: schema.readOnly || typeInfo.readOrWriteOnly === "
|
|
1320
|
-
writeOnly: schema.writeOnly || typeInfo.readOrWriteOnly === "
|
|
1334
|
+
readOnly: schema.readOnly || typeInfo.readOrWriteOnly === "🆁",
|
|
1335
|
+
writeOnly: schema.writeOnly || typeInfo.readOrWriteOnly === "🆆",
|
|
1321
1336
|
html: typeInfo.html || "",
|
|
1322
1337
|
};
|
|
1323
1338
|
}
|
|
@@ -1502,6 +1517,10 @@ function composeSchemaAST(schema, level, name, isRequired, suffix) {
|
|
|
1502
1517
|
optAst.writeOnly = false;
|
|
1503
1518
|
optAst.optionIndex = idx + 1;
|
|
1504
1519
|
optAst.optionTitle = optTitle;
|
|
1520
|
+
if (optAst.kind === "primitive" && !optTitle) {
|
|
1521
|
+
// The OPT n badge already names it, a row labelled "Option n" would only repeat it
|
|
1522
|
+
optAst.name = "";
|
|
1523
|
+
}
|
|
1505
1524
|
}
|
|
1506
1525
|
return optAst;
|
|
1507
1526
|
})
|
|
@@ -1877,6 +1896,14 @@ function getSerializeStyleForContentType(contentType) {
|
|
|
1877
1896
|
return null;
|
|
1878
1897
|
}
|
|
1879
1898
|
|
|
1899
|
+
/**
|
|
1900
|
+
* Text that names an example in an examples dropdown: its summary, or its key when it has none.
|
|
1901
|
+
* A long summary is shown whole; the dropdown wraps it, or cuts it where the browser's select can't wrap.
|
|
1902
|
+
*/
|
|
1903
|
+
export function exampleLabel(example) {
|
|
1904
|
+
return example?.exampleSummary || example?.exampleId || "";
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1880
1907
|
export function getSchemaFromParam(param) {
|
|
1881
1908
|
if (param.schema) {
|
|
1882
1909
|
return [param.schema, null, null];
|
package/src/utils/slugger.js
CHANGED
|
@@ -129,23 +129,21 @@ export class OperationSlugger {
|
|
|
129
129
|
return this.opMap.get(opKey);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
// Hierarchical style prefixes with tag if provided; `used` holds the prefixed slugs
|
|
133
|
+
const prefix = tagSlug && tagSlug !== "general" ? `${tagSlug}--` : "";
|
|
134
|
+
const base = this.slugOp(op);
|
|
135
|
+
let candidate = `${prefix}${base}`;
|
|
133
136
|
|
|
134
137
|
// Smart semantic collision differentiator:
|
|
135
138
|
// If the base candidate has already been seen in this spec, attempt method-prefix first (e.g. post-webhooks)
|
|
136
139
|
if (this.used.has(candidate)) {
|
|
137
140
|
const method = (op.method || "get").toLowerCase();
|
|
138
|
-
const methodCandidate = `${method}-${
|
|
141
|
+
const methodCandidate = `${prefix}${method}-${base}`;
|
|
139
142
|
if (!this.used.has(methodCandidate)) {
|
|
140
143
|
candidate = methodCandidate;
|
|
141
144
|
}
|
|
142
145
|
}
|
|
143
146
|
|
|
144
|
-
// Hierarchical style prefixes with tag if provided
|
|
145
|
-
if (tagSlug && tagSlug !== "general") {
|
|
146
|
-
candidate = `${tagSlug}--${candidate}`;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
147
|
// Guaranteed uniqueness via GitHub slugger registry
|
|
150
148
|
const finalSlug = this.slugger.slug(candidate);
|
|
151
149
|
this.used.add(finalSlug);
|
package/src/utils/spec-parser.js
CHANGED
|
@@ -169,6 +169,7 @@ export default async function ProcessSpec(
|
|
|
169
169
|
matchPaths = "",
|
|
170
170
|
matchType = "",
|
|
171
171
|
removeEndpointsWithBadgeLabelAs = "",
|
|
172
|
+
showUnusedTags = false,
|
|
172
173
|
) {
|
|
173
174
|
let jsonParsedSpec;
|
|
174
175
|
try {
|
|
@@ -274,139 +275,163 @@ export default async function ProcessSpec(
|
|
|
274
275
|
};
|
|
275
276
|
}
|
|
276
277
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
278
|
+
// Everything after loading is also guarded: a spec that parses but trips the grouping code
|
|
279
|
+
// (a server without `url`, a tag without `name`, ...) still yields a `specLoadError` result
|
|
280
|
+
try {
|
|
281
|
+
const hasTagGroups = Boolean(
|
|
282
|
+
(jsonParsedSpec?.["x-tagGroups"] && jsonParsedSpec["x-tagGroups"].length > 0) ||
|
|
283
|
+
(jsonParsedSpec?.["x-tag-groups"] && jsonParsedSpec["x-tag-groups"].length > 0),
|
|
284
|
+
);
|
|
281
285
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
286
|
+
// Tags with Paths and WebHooks
|
|
287
|
+
const tags = groupByTags(
|
|
288
|
+
jsonParsedSpec,
|
|
289
|
+
sortEndpointsBy,
|
|
290
|
+
generateMissingTags,
|
|
291
|
+
sortTags,
|
|
292
|
+
hasTagGroups,
|
|
293
|
+
showUnusedTags,
|
|
294
|
+
);
|
|
290
295
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
// Components
|
|
295
|
-
const components = getComponents(jsonParsedSpec, sortSchemas);
|
|
296
|
-
|
|
297
|
-
// Info Description Headers
|
|
298
|
-
const infoDescriptionHeaders = jsonParsedSpec.info?.description
|
|
299
|
-
? getHeadersFromMarkdown(jsonParsedSpec.info.description)
|
|
300
|
-
: [];
|
|
301
|
-
|
|
302
|
-
// Security Scheme
|
|
303
|
-
const securitySchemes = [];
|
|
304
|
-
if (jsonParsedSpec.components?.securitySchemes) {
|
|
305
|
-
const securitySchemeSet = new Set();
|
|
306
|
-
Object.entries(jsonParsedSpec.components.securitySchemes).forEach((kv) => {
|
|
307
|
-
if (!securitySchemeSet.has(kv[0])) {
|
|
308
|
-
securitySchemeSet.add(kv[0]);
|
|
309
|
-
const securityObj = { securitySchemeId: kv[0], ...kv[1] };
|
|
310
|
-
securityObj.in = "header";
|
|
311
|
-
securityObj.name = "Authorization"; // Name of the header/cookie/api-key
|
|
312
|
-
securityObj.nameId = kv[1].name || kv[0]; // Name of the security-scheme
|
|
313
|
-
securityObj.user = "";
|
|
314
|
-
securityObj.password = "";
|
|
315
|
-
securityObj.clientId = "";
|
|
316
|
-
securityObj.clientSecret = "";
|
|
317
|
-
securityObj.value = "";
|
|
318
|
-
securityObj.finalKeyValue = "";
|
|
319
|
-
if (kv[1].type === "apiKey") {
|
|
320
|
-
securityObj.in = kv[1].in || "header";
|
|
321
|
-
securityObj.name = kv[1].name || "Authorization";
|
|
322
|
-
}
|
|
323
|
-
securitySchemes.push(securityObj);
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
}
|
|
296
|
+
// 3-Level Tag Groups (x-tagGroups / x-tag-groups)
|
|
297
|
+
const tagGroups = groupByTagGroups(jsonParsedSpec, tags);
|
|
327
298
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
securitySchemeId: rapidocApiKey,
|
|
331
|
-
description: "api-key provided in rapidoc element attributes",
|
|
332
|
-
type: "apiKey",
|
|
333
|
-
oAuthFlow: "",
|
|
334
|
-
name: attrApiKey, // Name of the header/cookie/api-key
|
|
335
|
-
nameId: attrApiKey, // Name of the security-scheme
|
|
336
|
-
in: attrApiKeyLocation,
|
|
337
|
-
value: attrApiKeyValue,
|
|
338
|
-
finalKeyValue: attrApiKeyValue,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
299
|
+
// Components
|
|
300
|
+
const components = getComponents(jsonParsedSpec, sortSchemas);
|
|
341
301
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
} else if (v.type === "apiKey") {
|
|
347
|
-
v.typeDisplay = `API Key (${v.name})`;
|
|
348
|
-
} else if (v.type === "oauth2") {
|
|
349
|
-
v.typeDisplay = `OAuth (${v.securitySchemeId})`;
|
|
350
|
-
} else {
|
|
351
|
-
v.typeDisplay = v.type || "None";
|
|
352
|
-
}
|
|
353
|
-
});
|
|
302
|
+
// Info Description Headers
|
|
303
|
+
const infoDescriptionHeaders = jsonParsedSpec.info?.description
|
|
304
|
+
? getHeadersFromMarkdown(jsonParsedSpec.info.description)
|
|
305
|
+
: [];
|
|
354
306
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
307
|
+
// Security Scheme
|
|
308
|
+
const securitySchemes = [];
|
|
309
|
+
if (jsonParsedSpec.components?.securitySchemes) {
|
|
310
|
+
const securitySchemeSet = new Set();
|
|
311
|
+
Object.entries(jsonParsedSpec.components.securitySchemes).forEach((kv) => {
|
|
312
|
+
if (!securitySchemeSet.has(kv[0])) {
|
|
313
|
+
securitySchemeSet.add(kv[0]);
|
|
314
|
+
const securityObj = { securitySchemeId: kv[0], ...kv[1] };
|
|
315
|
+
securityObj.in = "header";
|
|
316
|
+
securityObj.name = "Authorization"; // Name of the header/cookie/api-key
|
|
317
|
+
securityObj.nameId = kv[1].name || kv[0]; // Name of the security-scheme
|
|
318
|
+
securityObj.user = "";
|
|
319
|
+
securityObj.password = "";
|
|
320
|
+
securityObj.clientId = "";
|
|
321
|
+
securityObj.clientSecret = "";
|
|
322
|
+
securityObj.value = "";
|
|
323
|
+
securityObj.finalKeyValue = "";
|
|
324
|
+
if (kv[1].type === "apiKey") {
|
|
325
|
+
securityObj.in = kv[1].in || "header";
|
|
326
|
+
securityObj.name = kv[1].name || "Authorization";
|
|
327
|
+
}
|
|
328
|
+
securitySchemes.push(securityObj);
|
|
374
329
|
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (attrApiKey && attrApiKeyLocation && attrApiKeyValue) {
|
|
334
|
+
securitySchemes.push({
|
|
335
|
+
securitySchemeId: rapidocApiKey,
|
|
336
|
+
description: "api-key provided in rapidoc element attributes",
|
|
337
|
+
type: "apiKey",
|
|
338
|
+
oAuthFlow: "",
|
|
339
|
+
name: attrApiKey, // Name of the header/cookie/api-key
|
|
340
|
+
nameId: attrApiKey, // Name of the security-scheme
|
|
341
|
+
in: attrApiKeyLocation,
|
|
342
|
+
value: attrApiKeyValue,
|
|
343
|
+
finalKeyValue: attrApiKeyValue,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Updated Security Type Display Text based on Type
|
|
348
|
+
securitySchemes.forEach((v) => {
|
|
349
|
+
if (v.type === "http") {
|
|
350
|
+
v.typeDisplay = v.scheme === "basic" ? "HTTP Basic" : `HTTP Bearer ${v.nameId}`;
|
|
351
|
+
} else if (v.type === "apiKey") {
|
|
352
|
+
v.typeDisplay = `API Key (${v.name})`;
|
|
353
|
+
} else if (v.type === "oauth2") {
|
|
354
|
+
v.typeDisplay = `OAuth (${v.securitySchemeId})`;
|
|
355
|
+
} else {
|
|
356
|
+
v.typeDisplay = v.type || "None";
|
|
375
357
|
}
|
|
376
|
-
// Apply server-variables to generate final computed-url
|
|
377
|
-
if (v.variables) {
|
|
378
|
-
Object.entries(v.variables).forEach((kv) => {
|
|
379
|
-
const regex = new RegExp(`{${kv[0]}}`, "g");
|
|
380
|
-
computedUrl = computedUrl.replace(regex, kv[1].default || "");
|
|
381
|
-
kv[1].value = kv[1].default || "";
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
v.computedUrl = computedUrl;
|
|
385
358
|
});
|
|
386
|
-
|
|
387
|
-
|
|
359
|
+
|
|
360
|
+
// Servers
|
|
361
|
+
let servers = [];
|
|
362
|
+
if (
|
|
363
|
+
jsonParsedSpec.servers &&
|
|
364
|
+
Array.isArray(jsonParsedSpec.servers) &&
|
|
365
|
+
jsonParsedSpec.servers.length > 0
|
|
366
|
+
) {
|
|
367
|
+
jsonParsedSpec.servers.forEach((v) => {
|
|
368
|
+
let computedUrl = String(v.url ?? "").trim();
|
|
369
|
+
if (
|
|
370
|
+
!(
|
|
371
|
+
computedUrl.startsWith("http") ||
|
|
372
|
+
computedUrl.startsWith("//") ||
|
|
373
|
+
computedUrl.startsWith("{")
|
|
374
|
+
)
|
|
375
|
+
) {
|
|
376
|
+
if (typeof window !== "undefined" && window.location?.origin?.startsWith("http")) {
|
|
377
|
+
v.url = window.location.origin + v.url;
|
|
378
|
+
computedUrl = v.url;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Apply server-variables to generate final computed-url
|
|
382
|
+
if (v.variables) {
|
|
383
|
+
Object.entries(v.variables).forEach((kv) => {
|
|
384
|
+
const regex = new RegExp(`{${kv[0]}}`, "g");
|
|
385
|
+
computedUrl = computedUrl.replace(regex, kv[1].default || "");
|
|
386
|
+
kv[1].value = kv[1].default || "";
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
v.computedUrl = computedUrl;
|
|
390
|
+
});
|
|
391
|
+
if (serverUrl) {
|
|
392
|
+
jsonParsedSpec.servers.push({ url: serverUrl, computedUrl: serverUrl });
|
|
393
|
+
}
|
|
394
|
+
} else if (serverUrl) {
|
|
395
|
+
jsonParsedSpec.servers = [{ url: serverUrl, computedUrl: serverUrl }];
|
|
396
|
+
} else if (typeof window !== "undefined" && window.location?.origin?.startsWith("http")) {
|
|
397
|
+
jsonParsedSpec.servers = [
|
|
398
|
+
{ url: window.location.origin, computedUrl: window.location.origin },
|
|
399
|
+
];
|
|
400
|
+
} else {
|
|
401
|
+
jsonParsedSpec.servers = [{ url: "http://localhost", computedUrl: "http://localhost" }];
|
|
388
402
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
403
|
+
servers = jsonParsedSpec.servers;
|
|
404
|
+
const parsedSpec = {
|
|
405
|
+
specLoadError: false,
|
|
406
|
+
isSpecLoading: false,
|
|
407
|
+
info: jsonParsedSpec.info,
|
|
408
|
+
infoDescriptionHeaders,
|
|
409
|
+
tags,
|
|
410
|
+
tagGroups,
|
|
411
|
+
components,
|
|
412
|
+
externalDocs: jsonParsedSpec.externalDocs,
|
|
413
|
+
securitySchemes,
|
|
414
|
+
servers,
|
|
415
|
+
};
|
|
416
|
+
return parsedSpec;
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.info(
|
|
419
|
+
"RapiDoc: %c There was an issue while processing the spec %o ",
|
|
420
|
+
"color:orangered",
|
|
421
|
+
err,
|
|
422
|
+
);
|
|
423
|
+
return {
|
|
424
|
+
specLoadError: true,
|
|
425
|
+
isSpecLoading: false,
|
|
426
|
+
info: {
|
|
427
|
+
title: "Error processing the spec",
|
|
428
|
+
description: `Unable to process the spec ┃ ${err?.message || err}`,
|
|
429
|
+
version: "",
|
|
430
|
+
},
|
|
431
|
+
tags: [],
|
|
432
|
+
tagGroups: [],
|
|
433
|
+
};
|
|
395
434
|
}
|
|
396
|
-
servers = jsonParsedSpec.servers;
|
|
397
|
-
const parsedSpec = {
|
|
398
|
-
specLoadError: false,
|
|
399
|
-
isSpecLoading: false,
|
|
400
|
-
info: jsonParsedSpec.info,
|
|
401
|
-
infoDescriptionHeaders,
|
|
402
|
-
tags,
|
|
403
|
-
tagGroups,
|
|
404
|
-
components,
|
|
405
|
-
externalDocs: jsonParsedSpec.externalDocs,
|
|
406
|
-
securitySchemes,
|
|
407
|
-
servers,
|
|
408
|
-
};
|
|
409
|
-
return parsedSpec;
|
|
410
435
|
}
|
|
411
436
|
|
|
412
437
|
function filterPaths(
|
|
@@ -615,6 +640,7 @@ function groupByTags(
|
|
|
615
640
|
generateMissingTags = false,
|
|
616
641
|
sortTags = false,
|
|
617
642
|
hasTagGroups = false,
|
|
643
|
+
showUnusedTags = false,
|
|
618
644
|
) {
|
|
619
645
|
const supportedMethods = [
|
|
620
646
|
"get",
|
|
@@ -645,45 +671,37 @@ function groupByTags(
|
|
|
645
671
|
}))
|
|
646
672
|
: [];
|
|
647
673
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}
|
|
654
|
-
}
|
|
674
|
+
// Paths first, then webhooks; a webhook named like a path does not replace it and the spec is not mutated
|
|
675
|
+
const pathsAndWebhooks = [
|
|
676
|
+
...Object.entries(openApiSpec.paths || {}).map(([name, item]) => [name, item, false]),
|
|
677
|
+
...Object.entries(openApiSpec.webhooks || {}).map(([name, item]) => [name, item, true]),
|
|
678
|
+
];
|
|
655
679
|
// For each path find the tag and push it into the corresponding tag
|
|
656
|
-
for (const pathOrHookName
|
|
657
|
-
const commonParams =
|
|
680
|
+
for (const [pathOrHookName, pathItem, isWebhook] of pathsAndWebhooks) {
|
|
681
|
+
const commonParams = pathItem.parameters;
|
|
658
682
|
const commonPathProp = {
|
|
659
|
-
servers:
|
|
660
|
-
parameters:
|
|
683
|
+
servers: pathItem.servers || [],
|
|
684
|
+
parameters: pathItem.parameters || [],
|
|
661
685
|
};
|
|
662
|
-
const isWebhook = pathsAndWebhooks[pathOrHookName]._type === "webhook";
|
|
663
686
|
const operationsToProcess = [];
|
|
664
687
|
supportedMethods.forEach((methodName) => {
|
|
665
|
-
if (
|
|
688
|
+
if (pathItem[methodName]) {
|
|
666
689
|
operationsToProcess.push({
|
|
667
690
|
methodName,
|
|
668
|
-
pathOrHookObj:
|
|
691
|
+
pathOrHookObj: pathItem[methodName],
|
|
669
692
|
});
|
|
670
693
|
}
|
|
671
694
|
});
|
|
672
695
|
|
|
673
|
-
if (
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
pathOrHookObj: customObj,
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
},
|
|
686
|
-
);
|
|
696
|
+
if (pathItem.additionalOperations && typeof pathItem.additionalOperations === "object") {
|
|
697
|
+
Object.entries(pathItem.additionalOperations).forEach(([customMethod, customObj]) => {
|
|
698
|
+
if (customObj && typeof customObj === "object") {
|
|
699
|
+
operationsToProcess.push({
|
|
700
|
+
methodName: customMethod.toLowerCase(),
|
|
701
|
+
pathOrHookObj: customObj,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
});
|
|
687
705
|
}
|
|
688
706
|
|
|
689
707
|
operationsToProcess.forEach(({ methodName, pathOrHookObj }) => {
|
|
@@ -709,7 +727,7 @@ function groupByTags(
|
|
|
709
727
|
let specTagsItem;
|
|
710
728
|
|
|
711
729
|
if (openApiSpec.tags) {
|
|
712
|
-
specTagsItem = openApiSpec.tags.find((v) => v.name
|
|
730
|
+
specTagsItem = openApiSpec.tags.find((v) => v.name?.toLowerCase() === tag.toLowerCase());
|
|
713
731
|
}
|
|
714
732
|
|
|
715
733
|
tagObj = tags.find((v) => v.name === tag);
|
|
@@ -816,23 +834,31 @@ function groupByTags(
|
|
|
816
834
|
xBadges: pathOrHookObj["x-badges"] || undefined,
|
|
817
835
|
xCodeSamples: pathOrHookObj["x-codeSamples"] || pathOrHookObj["x-code-samples"] || "",
|
|
818
836
|
rawOperation: pathOrHookObj,
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
pathOrHookObj["x-internal"] ??
|
|
822
|
-
pathsAndWebhooks[pathOrHookName]?.["x-internal"] ??
|
|
823
|
-
undefined,
|
|
824
|
-
xHidden:
|
|
825
|
-
pathOrHookObj["x-hidden"] ??
|
|
826
|
-
pathsAndWebhooks[pathOrHookName]?.["x-hidden"] ??
|
|
827
|
-
undefined,
|
|
828
|
-
xOpenAiIsConsequential: pathOrHookObj["x-openai-isConsequential"],
|
|
829
|
-
xSpeakeasyNameOverride: pathOrHookObj["x-speakeasy-name-override"] || undefined,
|
|
837
|
+
xInternal: pathOrHookObj["x-internal"] ?? pathItem?.["x-internal"] ?? undefined,
|
|
838
|
+
xHidden: pathOrHookObj["x-hidden"] ?? pathItem?.["x-hidden"] ?? undefined,
|
|
830
839
|
});
|
|
831
840
|
}); // End of tag path create
|
|
832
841
|
}); // End of operationsToProcess
|
|
833
842
|
}
|
|
834
843
|
|
|
835
|
-
|
|
844
|
+
// A tag no operation uses is dropped, unless it has a description to show (a guide or concepts section) and
|
|
845
|
+
// either show-unused-tags is on or the spec lists the tag in x-tagGroups. Such a tag is marked descriptionOnly.
|
|
846
|
+
const tagNamesInGroups = new Set(
|
|
847
|
+
(openApiSpec["x-tagGroups"] || openApiSpec["x-tag-groups"] || [])
|
|
848
|
+
.flatMap((g) => (Array.isArray(g?.tags) ? g.tags : []))
|
|
849
|
+
.filter((name) => typeof name === "string")
|
|
850
|
+
.map((name) => name.toLowerCase()),
|
|
851
|
+
);
|
|
852
|
+
const tagsWithSortedPaths = tags.filter((tag) => {
|
|
853
|
+
if (tag.paths && tag.paths.length > 0) {
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
if (tag.description && (showUnusedTags || tagNamesInGroups.has(tag.name?.toLowerCase()))) {
|
|
857
|
+
tag.descriptionOnly = true;
|
|
858
|
+
return true;
|
|
859
|
+
}
|
|
860
|
+
return false;
|
|
861
|
+
});
|
|
836
862
|
tagsWithSortedPaths.forEach((tag) => {
|
|
837
863
|
if (sortEndpointsBy !== "none") {
|
|
838
864
|
if (sortEndpointsBy === "method") {
|
package/src/utils/url-utils.js
CHANGED
|
@@ -20,6 +20,29 @@ export function replacePathParams(pathTemplate = "", pathParams = {}) {
|
|
|
20
20
|
return resolvedPath;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* A value typed into an array input is a string; in a JSON array, numbers and booleans keep their JSON type.
|
|
25
|
+
*/
|
|
26
|
+
function typedArrayItem(value, itemType) {
|
|
27
|
+
if (typeof value !== "string") {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
// an OpenAPI 3.1 type list such as ["integer", "null"] arrives as "integer,null"
|
|
31
|
+
const types = String(itemType).split(",");
|
|
32
|
+
const text = value.trim();
|
|
33
|
+
if (
|
|
34
|
+
(types.includes("integer") || types.includes("number")) &&
|
|
35
|
+
text !== "" &&
|
|
36
|
+
Number.isFinite(Number(text))
|
|
37
|
+
) {
|
|
38
|
+
return Number(text);
|
|
39
|
+
}
|
|
40
|
+
if (types.includes("boolean") && (text === "true" || text === "false")) {
|
|
41
|
+
return text === "true";
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
/**
|
|
24
47
|
* Serializes a single query parameter into an array of [key, value] tuples based on OpenAPI style and explode rules.
|
|
25
48
|
*/
|
|
@@ -29,6 +52,7 @@ export function serializeQueryParam(name, rawValue, options = {}) {
|
|
|
29
52
|
explode = style === "form",
|
|
30
53
|
isArray = Array.isArray(rawValue),
|
|
31
54
|
isObject = false,
|
|
55
|
+
itemType = "",
|
|
32
56
|
} = options;
|
|
33
57
|
|
|
34
58
|
if (rawValue === undefined || rawValue === null || rawValue === "") {
|
|
@@ -95,6 +119,10 @@ export function serializeQueryParam(name, rawValue, options = {}) {
|
|
|
95
119
|
return [];
|
|
96
120
|
}
|
|
97
121
|
|
|
122
|
+
// A parameter described by content: application/json is one JSON array, not one parameter per value
|
|
123
|
+
if (style === "json") {
|
|
124
|
+
return [[name, JSON.stringify(vals.map((v) => typedArrayItem(v, itemType)))]];
|
|
125
|
+
}
|
|
98
126
|
if (style === "spaceDelimited") {
|
|
99
127
|
return [[name, vals.join(" ")]];
|
|
100
128
|
}
|
|
@@ -140,9 +168,16 @@ export function buildQueryString(queryParams = []) {
|
|
|
140
168
|
allowReserved = false,
|
|
141
169
|
isArray = false,
|
|
142
170
|
isObject = false,
|
|
171
|
+
itemType = "",
|
|
143
172
|
} = param;
|
|
144
173
|
|
|
145
|
-
const entries = serializeQueryParam(name, value, {
|
|
174
|
+
const entries = serializeQueryParam(name, value, {
|
|
175
|
+
style,
|
|
176
|
+
explode,
|
|
177
|
+
isArray,
|
|
178
|
+
isObject,
|
|
179
|
+
itemType,
|
|
180
|
+
});
|
|
146
181
|
|
|
147
182
|
for (const [k, v] of entries) {
|
|
148
183
|
const encodedKey = encodeURIComponent(k);
|