"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.responseSchema = exports.check = void 0;
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
/** How VALUE would be spelled as an OpenAPI type, for problem messages. */
const typeName = (value) => {
if (value === null || value === undefined)
return 'null';
if (Array.isArray(value))
return 'array';
if (typeof value === 'number')
return Number.isInteger(value) ? 'integer' : 'number';
return typeof value;
};
const typeMatches = (declared, value) => {
switch (declared) {
case 'object':
return isObject(value);
case 'array':
return Array.isArray(value);
case 'string':
return typeof value === 'string';
case 'integer':
return typeof value === 'number' && Number.isInteger(value);
case 'number':
return typeof value === 'number';
case 'boolean':
return typeof value === 'boolean';
case 'null':
return value === null || value === undefined;
default:
return true;
}
};
const show = (value) => JSON.stringify(value) ?? String(value);
const checkNode = (schema, value, path) => {
const { type, properties, required, items, oneOf, additionalProperties } = schema;
const allowed = schema.enum ?? ('const' in schema ? [schema.const] : undefined);
const types = type === undefined ? undefined : typeof type === 'string' ? [type] : type;
if (oneOf) {
return oneOf.some((alternative) => (0, exports.check)(alternative, value, path).length === 0)
? []
: [`${path} matches none of oneOf ${show(oneOf.map((a) => a.type))} — got ${typeName(value)}`];
}
if (allowed && !allowed.includes(value)) {
return [`${path} = ${show(value)} is not in the declared enum ${show(allowed)}`];
}
if (types && !types.some((t) => typeMatches(t, value))) {
return [`${path} is declared ${types.join('|')} but the API returned ${typeName(value)}`];
}
const problems = [];
const object = types?.includes('object') && isObject(value) ? value : undefined;
if (object) {
for (const key of required ?? []) {
if (!(key in object))
problems.push(`${path}.${key} is required by the spec but absent from the response`);
}
// the inverse, and the one that finds under-documented responses:
// additionalProperties:false makes an unlisted field a violation
if (additionalProperties === false && properties) {
for (const key of Object.keys(object)) {
if (!(key in properties))
problems.push(`${path}.${key} is returned by the API but absent from the spec`);
}
}
}
if (properties && isObject(value)) {
for (const [name, propertySchema] of Object.entries(properties)) {
if (name in value)
problems.push(...(0, exports.check)(propertySchema, value[name], `${path}.${name}`));
}
}
// every row, never a sample: a value that appears only in rare rows (an
// uncommon action type, say) is exactly the one a schema omits
if (items && Array.isArray(value)) {
value.forEach((row, i) => problems.push(...(0, exports.check)(items, row, `${path}[${i}]`)));
}
return problems;
};
/**
* Problems found validating VALUE against SCHEMA, as human-readable
* strings; empty when the response conforms. PATH prefixes each message so
* a problem can be located in a nested body.
*/
const check = (schema, value, path = '') => schema === undefined || Object.keys(schema).length === 0 ? [] : checkNode(schema, value, path);
exports.check = check;
/** The schema SPEC declares for METHOD PATH's STATUS response, or undefined when it documents no JSON body. */
const responseSchema = (spec, method, path, status) => spec.paths?.[path]?.[method]?.responses?.[String(status)]?.content?.['application/json']?.schema;
exports.responseSchema = responseSchema;