@scalar/oas-utils
Version:
Open API spec and Yaml handling utilities
354 lines (353 loc) • 11.8 kB
JavaScript
import { schemaModel } from "../../helpers/schema-model.js";
import { getServerVariableExamples } from "../../spec-getters/get-server-variable-examples.js";
import { keysOf } from "@scalar/object-utils/arrays";
import { nanoidSchema } from "@scalar/types/utils";
import { z } from "zod";
import { getRequestBodyFromOperation } from "../../spec-getters/get-request-body-from-operation.js";
import { isDefined } from "@scalar/helpers/array/is-defined";
import { objectKeys } from "@scalar/helpers/object/object-keys";
const requestExampleParametersSchema = z.object({
key: z.string().default(""),
value: z.coerce.string().default(""),
enabled: z.boolean().default(true),
file: z.any().optional(),
description: z.string().optional(),
required: z.boolean().optional(),
enum: z.array(z.string()).optional(),
examples: z.array(z.any()).optional(),
type: z.union([
// 'string'
z.string(),
// ['string', 'null']
z.array(z.string())
]).optional(),
format: z.string().optional(),
minimum: z.number().optional(),
maximum: z.number().optional(),
default: z.any().optional(),
nullable: z.boolean().optional()
}).transform((_data) => {
const data = { ..._data };
if (Array.isArray(data.type) && data.type.includes("null")) {
data.nullable = true;
}
if (Array.isArray(data.type) && data.type.length === 2 && data.type.includes("null")) {
data.type = data.type.find((item) => item !== "null");
}
return data;
});
const parameterArrayToObject = (params) => params.reduce((map, param) => {
map[param.key] = param.value;
return map;
}, {});
const xScalarFileValueSchema = z.object({
url: z.string(),
base64: z.string().optional()
}).nullable();
const xScalarFormDataValue = z.union([
z.object({
type: z.literal("string"),
value: z.string()
}),
z.object({
type: z.literal("file"),
file: xScalarFileValueSchema
})
]);
const exampleRequestBodyEncoding = ["json", "text", "html", "javascript", "xml", "yaml", "edn"];
const exampleBodyMime = [
"application/json",
"text/plain",
"text/html",
"application/javascript",
"application/xml",
"application/yaml",
"application/edn",
"application/octet-stream",
"application/x-www-form-urlencoded",
"multipart/form-data",
/** Used for direct files */
"binary"
];
const contentMapping = {
json: "application/json",
text: "text/plain",
html: "text/html",
javascript: "application/javascript",
xml: "application/xml",
yaml: "application/yaml",
edn: "application/edn"
};
const exampleRequestBodySchema = z.object({
raw: z.object({
encoding: z.enum(exampleRequestBodyEncoding),
value: z.string().default(""),
mimeType: z.string().optional()
}).optional(),
formData: z.object({
encoding: z.union([z.literal("form-data"), z.literal("urlencoded")]).default("form-data"),
value: requestExampleParametersSchema.array().default([])
}).optional(),
binary: z.instanceof(Blob).optional(),
activeBody: z.union([z.literal("raw"), z.literal("formData"), z.literal("binary")]).default("raw")
});
const xScalarExampleBodySchema = z.object({
encoding: z.enum(exampleBodyMime),
/**
* Body content as an object with a separately specified encoding or a simple pre-encoded string value
*
* Ideally we would convert any objects into the proper encoding on import
*/
content: z.union([z.record(z.string(), z.any()), z.string()]),
/** When the encoding is `binary` this will be used to link to the file */
file: xScalarFileValueSchema.optional()
});
const requestExampleSchema = z.object({
uid: nanoidSchema.brand(),
type: z.literal("requestExample").optional().default("requestExample"),
requestUid: z.string().brand().optional(),
name: z.string().optional().default("Name"),
body: exampleRequestBodySchema.optional().default({}),
parameters: z.object({
path: requestExampleParametersSchema.array().default([]),
query: requestExampleParametersSchema.array().default([]),
headers: requestExampleParametersSchema.array().default([{ key: "Accept", value: "*/*", enabled: true }]),
cookies: requestExampleParametersSchema.array().default([])
}).optional().default({}),
/** TODO: Should this be deprecated? */
serverVariables: z.record(z.string(), z.array(z.string())).optional()
});
const xScalarExampleParameterSchema = z.record(z.string(), z.string()).optional();
const xScalarExampleSchema = z.object({
/** TODO: Should this be required? */
name: z.string().optional(),
body: xScalarExampleBodySchema.optional(),
parameters: z.object({
path: xScalarExampleParameterSchema,
query: xScalarExampleParameterSchema,
headers: xScalarExampleParameterSchema,
cookies: xScalarExampleParameterSchema
})
});
function convertExampleToXScalar(example) {
const active = example.body?.activeBody;
const xScalarBody = {
encoding: "text/plain",
content: ""
};
if (example.body?.activeBody === "binary") {
xScalarBody.encoding = "binary";
xScalarBody.file = null;
}
if (active === "formData" && example.body?.[active]) {
const body = example.body[active];
xScalarBody.encoding = body.encoding === "form-data" ? "multipart/form-data" : "application/x-www-form-urlencoded";
xScalarBody.content = body.value.reduce((map, param) => {
map[param.key] = param.file ? {
type: "file",
file: null
} : {
type: "string",
value: param.value
};
return map;
}, {});
}
if (example.body?.activeBody === "raw") {
xScalarBody.encoding = contentMapping[example.body.raw?.encoding ?? "text"] ?? "text/plain";
xScalarBody.content = example.body.raw?.value ?? "";
}
const parameters = {};
keysOf(example.parameters ?? {}).forEach((key) => {
if (example.parameters?.[key].length) {
parameters[key] = parameterArrayToObject(example.parameters[key]);
}
});
return xScalarExampleSchema.parse({
/** Only add the body if we have content or the body should be a file */
body: xScalarBody.content || xScalarBody.encoding === "binary" ? xScalarBody : void 0,
parameters
});
}
function createParamInstance(param) {
const schema = param.schema;
const firstExample = (() => {
if (param.examples && !Array.isArray(param.examples) && objectKeys(param.examples).length > 0) {
const exampleValues = Object.entries(param.examples).map(([_, example2]) => {
if (example2.externalValue) {
return example2.externalValue;
}
return example2.value;
});
return { value: exampleValues[0], examples: exampleValues };
}
if (isDefined(param.example)) {
return { value: param.example };
}
if (Array.isArray(param.examples) && param.examples.length > 0) {
return { value: param.examples[0] };
}
if (isDefined(schema?.example)) {
return { value: schema.example };
}
if (Array.isArray(schema?.examples) && schema.examples.length > 0) {
if (schema?.type === "boolean") {
return { value: schema.default ?? false };
}
return { value: schema.examples[0] };
}
if (param.content) {
const firstContentType = objectKeys(param.content)[0];
if (firstContentType) {
const content = param.content[firstContentType];
if (content?.examples) {
const firstExampleKey = Object.keys(content.examples)[0];
if (firstExampleKey) {
const example2 = content.examples[firstExampleKey];
if (isDefined(example2?.value)) {
return { value: example2.value };
}
}
}
if (isDefined(content?.example)) {
return { value: content.example };
}
}
}
return null;
})();
const value = String(firstExample?.value ?? schema?.default ?? "");
const parseEnum = (() => {
if (schema?.enum && schema?.type !== "string") {
return schema.enum?.map(String);
}
if (schema?.items?.enum && schema?.type === "array") {
return schema.items.enum.map(String);
}
return schema?.enum;
})();
const examples = firstExample?.examples || (schema?.examples && schema?.type !== "string" ? schema.examples?.map(String) : schema?.examples);
const example = schemaModel(
{
...schema,
key: param.name,
value,
description: param.description,
required: param.required,
/** Initialized all required properties to enabled */
enabled: !!param.required,
enum: parseEnum,
examples
},
requestExampleParametersSchema,
false
);
if (!example) {
console.warn(`Example at ${param.name} is invalid.`);
return requestExampleParametersSchema.parse({});
}
return example;
}
function createExampleFromRequest(request, name, server) {
const parameters = {
path: [],
query: [],
cookie: [],
// deprecated TODO: add zod transform to remove
header: [],
headers: [{ key: "Accept", value: "*/*", enabled: true }]
};
request.parameters?.forEach((p) => parameters[p.in].push(createParamInstance(p)));
if (parameters.header.length > 0) {
parameters.headers = parameters.header;
parameters.header = [];
}
const contentTypeHeader = parameters.headers.find((h) => h.key.toLowerCase() === "content-type");
const body = {
activeBody: "raw"
};
if (request.requestBody || contentTypeHeader?.value) {
const requestBody = getRequestBodyFromOperation(request);
const contentType = request.requestBody ? requestBody?.mimeType : contentTypeHeader?.value;
if (contentType?.includes("/json") || contentType?.endsWith("+json")) {
body.activeBody = "raw";
body.raw = {
encoding: "json",
mimeType: contentType,
value: requestBody?.text ?? JSON.stringify({})
};
}
if (contentType === "application/xml") {
body.activeBody = "raw";
body.raw = {
encoding: "xml",
value: requestBody?.text ?? ""
};
}
if (contentType === "application/octet-stream") {
body.activeBody = "binary";
body.binary = void 0;
}
if (contentType === "application/x-www-form-urlencoded" || contentType === "multipart/form-data") {
body.activeBody = "formData";
body.formData = {
encoding: contentType === "application/x-www-form-urlencoded" ? "urlencoded" : "form-data",
value: (requestBody?.params || []).map((param) => {
if (param.value instanceof File) {
return {
key: param.name,
value: "BINARY",
file: param.value,
enabled: true
};
}
return {
key: param.name,
value: param.value || "",
enabled: true
};
})
};
}
if (requestBody?.mimeType && !contentTypeHeader && !requestBody.mimeType.startsWith("multipart/")) {
parameters.headers.push({
key: "Content-Type",
value: requestBody.mimeType,
enabled: true
});
}
}
const serverVariables = server ? getServerVariableExamples(server) : {};
const example = schemaModel(
{
requestUid: request.uid,
parameters,
name,
body,
serverVariables
},
requestExampleSchema,
false
);
if (!example) {
console.warn(`Example at ${request.uid} is invalid.`);
return requestExampleSchema.parse({});
}
return example;
}
export {
convertExampleToXScalar,
createExampleFromRequest,
createParamInstance,
exampleBodyMime,
exampleRequestBodyEncoding,
exampleRequestBodySchema,
parameterArrayToObject,
requestExampleParametersSchema,
requestExampleSchema,
xScalarExampleBodySchema,
xScalarExampleSchema,
xScalarFileValueSchema,
xScalarFormDataValue
};
//# sourceMappingURL=request-examples.js.map