UNPKG

counterfact

Version:

Generate a TypeScript-based mock server from an OpenAPI spec in seconds — with stateful routes, hot reload, and REPL support.

100 lines (99 loc) 4.32 kB
import Ajv from "ajv"; const ajv = new Ajv({ allErrors: true, strict: false, coerceTypes: false, }); /** * Returns `true` when a query parameter should be serialized as exploded form * style — meaning its object properties appear as individual query parameters * instead of being passed under the parameter's own name. * * Per OpenAPI 3.x: for `in: query`, the default `style` is `form` and the * default `explode` for `form` is `true`. An object-type parameter with these * defaults (or with them set explicitly) uses exploded form serialization. */ export function isExplodedObjectQueryParam(parameter) { if (parameter.in !== "query") return false; const schema = parameter.schema; if (!schema) return false; // Must be an object type (explicit type or implied by presence of properties) const isObjectType = schema.type === "object" || schema.properties !== undefined; if (!isObjectType) return false; // style must be "form" (the default for query params) or unset if (parameter.style !== undefined && parameter.style !== "form") return false; // explode must not be explicitly false (default for form style is true) if (parameter.explode === false) return false; return true; } function findMissingRequired(parameters, location, values) { return parameters .filter((p) => p.in === location && p.required === true) .filter((p) => { // For exploded object query params the individual properties appear as // separate query parameters, so we check for those instead of the name. if (location === "query" && isExplodedObjectQueryParam(p)) { const properties = p.schema?.properties; if (!properties) { // Free-form object with no declared properties: treat as always // satisfied — we cannot know which keys belong to the parameter. return false; } // The parameter is "missing" only when none of its properties are present. return !Object.keys(properties).some((key) => key in values); } return !(p.name in values) || values[p.name] === undefined; }) .map((p) => `${location} parameter '${p.name}' is required`); } export function validateRequest(operation, request) { if (!operation) { return { errors: [], valid: true }; } const errors = []; const parameters = operation.parameters ?? []; // For query and header parameters, HTTP always delivers values as strings. // Only check that required parameters are present; type coercion is handled // by the registry before the route handler is called. errors.push(...findMissingRequired(parameters, "query", request.query)); errors.push(...findMissingRequired(parameters, "header", request.headers)); errors.push(...findMissingRequired(parameters, "cookie", request.cookie)); // Validate request body (OpenAPI 3.x requestBody) if (operation.requestBody?.content !== undefined) { const schema = operation.requestBody.content["application/json"]?.schema ?? operation.requestBody.content["application/x-www-form-urlencoded"] ?.schema; if (schema !== undefined) { const valid = ajv.validate(schema, request.body); if (!valid && ajv.errors) { for (const error of ajv.errors) { const path = error.instancePath ?? ""; errors.push(`body${path} ${error.message ?? "is invalid"}`); } } } else if (operation.requestBody.required === true && !request.body) { errors.push("body is required"); } } // Validate request body (OpenAPI 2.x body parameter) const bodyParam = parameters.find((p) => p.in === "body"); if (bodyParam?.schema !== undefined) { const valid = ajv.validate(bodyParam.schema, request.body); if (!valid && ajv.errors) { for (const error of ajv.errors) { const path = error.instancePath ?? ""; errors.push(`body${path} ${error.message ?? "is invalid"}`); } } } return { errors, valid: errors.length === 0, }; }