190 lines
6.1 KiB
Plaintext
190 lines
6.1 KiB
Plaintext
import { isNamedType, SpecExtension } from '../../types';
|
|
import { oasTypeOf, matchesJsonSchemaType, getSuggest, validateSchemaEnumType } from '../utils';
|
|
import { isRef } from '../../ref-utils';
|
|
import { isPlainObject } from '../../utils';
|
|
|
|
import type { UserContext } from '../../walk';
|
|
import type {
|
|
Oas3Rule,
|
|
Oas2Rule,
|
|
Async2Rule,
|
|
Async3Rule,
|
|
Arazzo1Rule,
|
|
Overlay1Rule,
|
|
} from '../../visitors';
|
|
|
|
export const Struct:
|
|
| Oas3Rule
|
|
| Oas2Rule
|
|
| Async2Rule
|
|
| Async3Rule
|
|
| Arazzo1Rule
|
|
| Overlay1Rule = () => {
|
|
return {
|
|
any(
|
|
node: any,
|
|
{ report, type, location, rawLocation, key, resolve, ignoreNextVisitorsOnNode }
|
|
) {
|
|
const nodeType = oasTypeOf(node);
|
|
const refLocation = rawLocation !== location ? rawLocation : undefined;
|
|
|
|
if (type.items) {
|
|
if (nodeType !== 'array') {
|
|
report({
|
|
message: `Expected type \`${type.name}\` (array) but got \`${nodeType}\``,
|
|
from: refLocation,
|
|
});
|
|
ignoreNextVisitorsOnNode();
|
|
}
|
|
return;
|
|
} else if (nodeType !== 'object') {
|
|
if (type !== SpecExtension) {
|
|
// do not validate unknown extensions structure
|
|
report({
|
|
message: `Expected type \`${type.name}\` (object) but got \`${nodeType}\``,
|
|
from: refLocation,
|
|
});
|
|
}
|
|
ignoreNextVisitorsOnNode();
|
|
return;
|
|
}
|
|
|
|
const required =
|
|
typeof type.required === 'function' ? type.required(node, key) : type.required;
|
|
|
|
for (const propName of required || []) {
|
|
if (!(node as object).hasOwnProperty(propName)) {
|
|
report({
|
|
message: `The field \`${propName}\` must be present on this level.`,
|
|
from: refLocation,
|
|
location: [{ reportOnKey: true }],
|
|
});
|
|
}
|
|
}
|
|
|
|
const allowed = type.allowed?.(node);
|
|
if (allowed && isPlainObject(node)) {
|
|
for (const propName in node) {
|
|
if (
|
|
allowed.includes(propName) ||
|
|
(type.extensionsPrefix && propName.startsWith(type.extensionsPrefix)) ||
|
|
!Object.keys(type.properties).includes(propName)
|
|
) {
|
|
continue;
|
|
}
|
|
report({
|
|
message: `The field \`${propName}\` is not allowed here.`,
|
|
from: refLocation,
|
|
location: location.child([propName]).key(),
|
|
});
|
|
}
|
|
}
|
|
|
|
const requiredOneOf = type.requiredOneOf || null;
|
|
if (requiredOneOf) {
|
|
let hasProperty = false;
|
|
for (const propName of requiredOneOf || []) {
|
|
if ((node as object).hasOwnProperty(propName)) {
|
|
hasProperty = true;
|
|
}
|
|
}
|
|
if (!hasProperty)
|
|
report({
|
|
message: `Must contain at least one of the following fields: ${type.requiredOneOf?.join(
|
|
', '
|
|
)}.`,
|
|
from: refLocation,
|
|
location: [{ reportOnKey: true }],
|
|
});
|
|
}
|
|
|
|
for (const propName of Object.keys(node)) {
|
|
const propLocation = location.child([propName]);
|
|
let propValue = node[propName];
|
|
|
|
let propType = type.properties[propName];
|
|
if (propType === undefined) propType = type.additionalProperties;
|
|
if (typeof propType === 'function') propType = propType(propValue, propName);
|
|
|
|
if (isNamedType(propType)) {
|
|
continue; // do nothing for named schema, it is executed with the next any call
|
|
}
|
|
|
|
const propSchema = propType;
|
|
const propValueType = oasTypeOf(propValue);
|
|
|
|
if (propSchema === undefined) {
|
|
if (propName.startsWith('x-')) continue;
|
|
report({
|
|
message: `Property \`${propName}\` is not expected here.`,
|
|
suggest: getSuggest(propName, Object.keys(type.properties)),
|
|
from: refLocation,
|
|
location: propLocation.key(),
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (propSchema === null) {
|
|
continue; // just defined, no validation
|
|
}
|
|
|
|
if (propSchema.resolvable !== false && isRef(propValue)) {
|
|
propValue = resolve(propValue).node;
|
|
}
|
|
|
|
if (propSchema.items && propSchema.items?.enum && Array.isArray(propValue)) {
|
|
for (let i = 0; i < propValue.length; i++) {
|
|
validateSchemaEnumType(propSchema.items?.enum, propValue[i], propName, refLocation, {
|
|
report,
|
|
location: location.child([propName, i]),
|
|
} as UserContext);
|
|
}
|
|
}
|
|
|
|
if (propSchema.enum) {
|
|
validateSchemaEnumType(propSchema.enum, propValue, propName, refLocation, {
|
|
report,
|
|
location: location.child([propName]),
|
|
} as UserContext);
|
|
} else if (propSchema.type && !matchesJsonSchemaType(propValue, propSchema.type, false)) {
|
|
report({
|
|
message: `Expected type \`${propSchema.type}\` but got \`${propValueType}\`.`,
|
|
from: refLocation,
|
|
location: propLocation,
|
|
});
|
|
ignoreNextVisitorsOnNode();
|
|
} else if (propValueType === 'array' && propSchema.items?.type) {
|
|
const itemsType = propSchema.items?.type;
|
|
for (let i = 0; i < propValue.length; i++) {
|
|
const item = propValue[i];
|
|
if (!matchesJsonSchemaType(item, itemsType, false)) {
|
|
report({
|
|
message: `Expected type \`${itemsType}\` but got \`${oasTypeOf(item)}\`.`,
|
|
from: refLocation,
|
|
location: propLocation.child([i]),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (typeof propSchema.minimum === 'number') {
|
|
if (propSchema.minimum > node[propName]) {
|
|
report({
|
|
message: `The value of the ${propName} field must be greater than or equal to ${propSchema.minimum}`,
|
|
from: refLocation,
|
|
location: location.child([propName]),
|
|
});
|
|
}
|
|
}
|
|
|
|
if (propName === 'nullable' && !node.type) {
|
|
report({
|
|
message: 'The `type` field must be defined when the `nullable` field is used.',
|
|
location: location.child([propName]),
|
|
});
|
|
}
|
|
}
|
|
},
|
|
};
|
|
};
|