rapidoc
Version:
RapiDoc - Open API spec viewer with built in console
265 lines (237 loc) • 8.42 kB
JavaScript
/**
* Serializes a header parameter value according to OpenAPI 3 rules.
* Objects can be serialized exploded (key=value,key2=value2) or unexploded (key,value,key2,value2).
*
* @param {any} rawValue - Raw header parameter value
* @param {object} [options]
* @param {boolean} [options.explode=false] - Whether to explode object properties with '='
* @param {boolean} [options.isObject=false] - Whether value is an object
* @returns {string} Serialized header string
*/
export function serializeHeaderValue(rawValue, options = {}) {
const { explode = false, isObject = false } = options;
if (rawValue === undefined || rawValue === null || rawValue === "") {
return "";
}
if (isObject || (typeof rawValue === "object" && !Array.isArray(rawValue))) {
let obj = rawValue;
if (typeof rawValue === "string") {
try {
obj = JSON.parse(rawValue.replace(/\n/g, "").trim());
} catch {
return "";
}
}
if (!obj || typeof obj !== "object") {
return "";
}
const separator = explode ? "=" : ",";
return Object.keys(obj)
.map((key) => {
const val = obj[key];
const valStr = typeof val === "object" ? JSON.stringify(val) : String(val);
return `${key}${separator}${valStr}`;
})
.join(",");
}
if (Array.isArray(rawValue)) {
const filtered = rawValue.filter((v) => v !== "" && v !== undefined && v !== null);
return filtered.join(",");
}
return String(rawValue);
}
/**
* Constructs a Headers instance for an API request from structured inputs.
*
* @param {object} options
* @param {string} [options.accept] - Accept header
* @param {Array<object>} [options.apiKeys] - Applied API keys
* @param {Array<{ name: string, value: any, explode?: boolean, isObject?: boolean }>} [options.headerParams]
* @param {string} [options.contentType] - Content-Type to apply (omitted for multipart/form-data)
* @returns {Headers}
*/
export function constructFetchHeaders({
accept = "",
apiKeys = [],
headerParams = [],
contentType = "",
} = {}) {
const reqHeaders = new Headers();
if (accept) {
reqHeaders.append("Accept", accept);
}
if (Array.isArray(apiKeys)) {
apiKeys
.filter((k) => k && k.in === "header" && k.finalKeyValue)
.forEach((k) => {
reqHeaders.append(k.name, k.finalKeyValue);
});
}
if (Array.isArray(headerParams)) {
headerParams.forEach((param) => {
if (
param &&
param.name &&
param.value !== undefined &&
param.value !== null &&
param.value !== ""
) {
const headerValue = serializeHeaderValue(param.value, {
explode: param.explode === true || param.explode === "true",
isObject: param.isObject === true,
});
if (headerValue) {
reqHeaders.append(param.name, headerValue);
}
}
});
}
if (contentType && !contentType.includes("form-data")) {
reqHeaders.append("Content-Type", contentType);
}
return reqHeaders;
}
/**
* Builds a URLSearchParams body from field entries or a dynamic JSON template.
*
* @param {Array<{ name: string, value: any, isArray?: boolean }>} fields
* @param {string} [dynamicJson] - Raw JSON string for dynamic forms
* @returns {URLSearchParams}
*/
export function buildUrlEncodedBody(fields = [], dynamicJson = "") {
const params = new URLSearchParams();
if (dynamicJson) {
try {
const obj = JSON.parse(dynamicJson);
if (obj && typeof obj === "object") {
for (const prop of Object.keys(obj)) {
params.append(
prop,
typeof obj[prop] === "object" ? JSON.stringify(obj[prop]) : String(obj[prop]),
);
}
return params;
}
} catch {
// Fall through to fields
}
}
if (Array.isArray(fields)) {
fields.forEach((field) => {
if (
!field ||
!field.name ||
field.value === undefined ||
field.value === null ||
field.value === ""
) {
return;
}
if (field.isArray) {
const arr = Array.isArray(field.value) ? field.value : [field.value];
const filtered = arr.filter((v) => v !== "" && v !== undefined && v !== null);
if (filtered.length > 0) {
params.append(field.name, filtered.join(","));
}
} else {
params.append(field.name, String(field.value));
}
});
}
return params;
}
const isComplexSchema = (schema) =>
!!schema &&
(schema.type === "object" ||
(Array.isArray(schema.type) && schema.type.includes("object")) ||
(!schema.type && !!schema.properties));
/**
* Content-Type of a multipart/form-data part, following OpenAPI's "Special Considerations for multipart Content":
* the encoding object's contentType when the spec gives one (the first of a comma-separated list), application/json
* for an object or an array of objects, and "" otherwise (text/plain, or a file's own type).
*
* @param {object} schema - Schema of the form field
* @param {object} [encoding] - The media type's encoding entry for that field
* @returns {string}
*/
export function multipartPartContentType(schema, encoding) {
const declared =
typeof encoding?.contentType === "string" ? encoding.contentType.split(",")[0].trim() : "";
if (declared) {
return declared;
}
if (isComplexSchema(schema) || (schema?.type === "array" && isComplexSchema(schema.items))) {
return "application/json";
}
return "";
}
// Quotes and line breaks in part names and file names are percent-encoded, as browsers do for FormData
const escapePartName = (name) =>
String(name).replace(/"/g, "%22").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
/**
* Serializes FormData into a multipart/form-data body in which parts carry a Content-Type.
* A browser only sends a Content-Type for a part that is a file, and a Blob part always gets filename="blob",
* which servers read as an upload; so parts with a type in partContentTypes are written by hand.
*
* @param {FormData} formData
* @param {Object<string, string>} partContentTypes - Content-Type per field name, for fields that are not files
* @param {string} [boundary]
* @returns {{ body: Blob, contentType: string }}
*/
export function serializeMultipartBody(
formData,
partContentTypes = {},
boundary = `----RapiDocFormBoundary${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`,
) {
const chunks = [];
for (const [name, value] of formData.entries()) {
const isFile = typeof value !== "string";
let head = `--${boundary}\r\nContent-Disposition: form-data; name="${escapePartName(name)}"`;
if (isFile) {
head += `; filename="${escapePartName(value.name ?? "blob")}"\r\nContent-Type: ${value.type || partContentTypes[name] || "application/octet-stream"}`;
} else if (partContentTypes[name]) {
head += `\r\nContent-Type: ${partContentTypes[name]}`;
}
chunks.push(`${head}\r\n\r\n`, isFile ? value : value.replace(/\r?\n/g, "\r\n"), "\r\n");
}
chunks.push(`--${boundary}--\r\n`);
return { body: new Blob(chunks), contentType: `multipart/form-data; boundary=${boundary}` };
}
/**
* Builds a FormData instance from structured field entries.
*
* @param {Array<{ name: string, value: any, file?: File, isArray?: boolean }>} fields
* @returns {FormData}
*/
export function buildMultipartFormData(fields = []) {
const formData = new FormData();
if (!Array.isArray(fields)) {
return formData;
}
fields.forEach((field) => {
if (!field || !field.name) {
return;
}
if (typeof File !== "undefined" && field.file instanceof File) {
formData.append(field.name, field.file, field.file.name);
return;
}
if (field.value === undefined || field.value === null || field.value === "") {
return;
}
if (field.isArray) {
const arr = Array.isArray(field.value) ? field.value : [field.value];
const filtered = arr.filter((v) => v !== "" && v !== undefined && v !== null);
filtered.forEach((item) => {
if (typeof File !== "undefined" && item instanceof File) {
formData.append(field.name, item, item.name);
} else {
formData.append(field.name, String(item));
}
});
} else {
formData.append(field.name, String(field.value));
}
});
return formData;
}