UNPKG

@scalar/oas-utils

Version:

Open API spec and Yaml handling utilities

354 lines (353 loc) 11.8 kB
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