@redocly/openapi-core
Version:
See https://github.com/Redocly/redocly-cli
366 lines (354 loc) • 11.7 kB
text/typescript
import { isPlainObject, isString as runOnValue, isTruthy } from '../../../utils';
import { isOrdered, getIntersectionLength, regexFromString } from './utils';
import type { AssertionContext, AssertResult, CustomFunction } from '../../../config/types';
import type { Location } from '../../../ref-utils';
import type { OrderOptions, OrderDirection } from './utils';
export type AssertionFnContext = AssertionContext & { baseLocation: Location; rawValue?: any };
export type AssertionFn = (value: any, condition: any, ctx: AssertionFnContext) => AssertResult[];
export type Asserts = {
pattern: AssertionFn;
notPattern: AssertionFn;
enum: AssertionFn;
defined: AssertionFn;
required: AssertionFn;
disallowed: AssertionFn;
undefined: AssertionFn;
nonEmpty: AssertionFn;
minLength: AssertionFn;
maxLength: AssertionFn;
casing: AssertionFn;
sortOrder: AssertionFn;
mutuallyExclusive: AssertionFn;
mutuallyRequired: AssertionFn;
requireAny: AssertionFn;
ref: AssertionFn;
const: AssertionFn;
};
export const runOnKeysSet = new Set<keyof Asserts>([
'mutuallyExclusive',
'mutuallyRequired',
'enum',
'pattern',
'notPattern',
'minLength',
'maxLength',
'casing',
'sortOrder',
'disallowed',
'required',
'requireAny',
'ref',
'const',
'defined', // In case if `property` for assertions is not added
]);
export const runOnValuesSet = new Set<keyof Asserts>([
'pattern',
'notPattern',
'enum',
'defined',
'undefined',
'nonEmpty',
'minLength',
'maxLength',
'casing',
'sortOrder',
'ref',
'const',
]);
export const asserts: Asserts = {
pattern: (
value: string | string[],
condition: string,
{ baseLocation, rawValue }: AssertionFnContext
) => {
if (typeof value === 'undefined' || isPlainObject(value)) return []; // property doesn't exist or is an object, no need to lint it with this assert
const values = Array.isArray(value) ? value : [value];
const regex = regexFromString(condition);
return values
.map(
(_val) =>
!regex?.test(_val) && {
message: `"${_val}" should match a regex ${condition}`,
location: runOnValue(value)
? baseLocation
: isPlainObject(rawValue)
? baseLocation.child(_val).key()
: baseLocation.key(),
}
)
.filter(isTruthy);
},
notPattern: (
value: string | string[],
condition: string,
{ baseLocation, rawValue }: AssertionFnContext
) => {
if (typeof value === 'undefined' || isPlainObject(value)) return []; // property doesn't exist or is an object, no need to lint it with this assert
const values = Array.isArray(value) ? value : [value];
const regex = regexFromString(condition);
return values
.map(
(_val) =>
regex?.test(_val) && {
message: `"${_val}" should not match a regex ${condition}`,
location: runOnValue(value)
? baseLocation
: isPlainObject(rawValue)
? baseLocation.child(_val).key()
: baseLocation.key(),
}
)
.filter(isTruthy);
},
enum: (value: string | string[], condition: string[], { baseLocation }: AssertionFnContext) => {
if (typeof value === 'undefined' || isPlainObject(value)) return []; // property doesn't exist or is an object, no need to lint it with this assert
const values = Array.isArray(value) ? value : [value];
return values
.map(
(_val) =>
!condition.includes(_val) && {
message: `"${_val}" should be one of the predefined values`,
location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(),
}
)
.filter(isTruthy);
},
defined: (
value: string | undefined,
condition: boolean = true,
{ baseLocation }: AssertionFnContext
) => {
const isDefined = typeof value !== 'undefined';
const isValid = condition ? isDefined : !isDefined;
return isValid
? []
: [
{
message: condition ? `Should be defined` : 'Should be not defined',
location: baseLocation,
},
];
},
required: (value: string[], keys: string[], { baseLocation }: AssertionFnContext) => {
return keys
.map(
(requiredKey) =>
!value.includes(requiredKey) && {
message: `${requiredKey} is required`,
location: baseLocation.key(),
}
)
.filter(isTruthy);
},
disallowed: (
value: string | string[],
condition: string[],
{ baseLocation }: AssertionFnContext
) => {
if (typeof value === 'undefined' || isPlainObject(value)) return []; // property doesn't exist or is an object, no need to lint it with this assert
const values = Array.isArray(value) ? value : [value];
return values
.map(
(_val) =>
condition.includes(_val) && {
message: `"${_val}" is disallowed`,
location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(),
}
)
.filter(isTruthy);
},
const: (
value: string | number | boolean | string[] | number[],
condition: string | number | boolean,
{ baseLocation }: AssertionFnContext
) => {
if (typeof value === 'undefined') return [];
if (Array.isArray(value)) {
return value
.map(
(_val) =>
condition !== _val && {
message: `"${_val}" should be equal ${condition} `,
location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(),
}
)
.filter(isTruthy);
} else {
return value !== condition
? [
{
message: `${value} should be equal ${condition}`,
location: baseLocation,
},
]
: [];
}
},
undefined: (value: unknown, condition: boolean = true, { baseLocation }: AssertionFnContext) => {
const isUndefined = typeof value === 'undefined';
const isValid = condition ? isUndefined : !isUndefined;
return isValid
? []
: [
{
message: condition ? `Should not be defined` : 'Should be defined',
location: baseLocation,
},
];
},
nonEmpty: (
value: string | undefined | null,
condition: boolean = true,
{ baseLocation }: AssertionFnContext
) => {
const isEmpty = typeof value === 'undefined' || value === null || value === '';
const isValid = condition ? !isEmpty : isEmpty;
return isValid
? []
: [
{
message: condition ? `Should not be empty` : 'Should be empty',
location: baseLocation,
},
];
},
minLength: (value: string | any[], condition: number, { baseLocation }: AssertionFnContext) => {
if (typeof value === 'undefined' || value.length >= condition) return []; // property doesn't exist, no need to lint it with this assert
return [
{
message: `Should have at least ${condition} characters`,
location: baseLocation,
},
];
},
maxLength: (value: string | any[], condition: number, { baseLocation }: AssertionFnContext) => {
if (typeof value === 'undefined' || value.length <= condition) return []; // property doesn't exist, no need to lint it with this assert
return [
{
message: `Should have at most ${condition} characters`,
location: baseLocation,
},
];
},
casing: (value: string | string[], condition: string, { baseLocation }: AssertionFnContext) => {
if (typeof value === 'undefined' || isPlainObject(value)) return []; // property doesn't exist or is an object, no need to lint it with this assert
const values = Array.isArray(value) ? value : [value];
const casingRegexes: Record<string, RegExp> = {
camelCase: /^[a-z][a-zA-Z0-9]*$/g,
'kebab-case': /^([a-z][a-z0-9]*)(-[a-z0-9]+)*$/g,
snake_case: /^([a-z][a-z0-9]*)(_[a-z0-9]+)*$/g,
PascalCase: /^[A-Z][a-zA-Z0-9]+$/g,
MACRO_CASE: /^([A-Z][A-Z0-9]*)(_[A-Z0-9]+)*$/g,
'COBOL-CASE': /^([A-Z][A-Z0-9]*)(-[A-Z0-9]+)*$/g,
flatcase: /^[a-z][a-z0-9]+$/g,
};
return values
.map(
(_val) =>
!_val.match(casingRegexes[condition]) && {
message: `"${_val}" should use ${condition}`,
location: runOnValue(value) ? baseLocation : baseLocation.child(_val).key(),
}
)
.filter(isTruthy);
},
sortOrder: (
value: unknown[],
condition: OrderOptions | OrderDirection,
{ baseLocation }: AssertionFnContext
) => {
const direction = (condition as OrderOptions).direction || (condition as OrderDirection);
const property = (condition as OrderOptions).property;
if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' && !property) {
return [
{
message: `Please define a property to sort objects by`,
location: baseLocation,
},
];
}
if (typeof value === 'undefined' || isOrdered(value, condition)) return [];
return [
{
message: `Should be sorted in ${
direction === 'asc' ? 'an ascending' : 'a descending'
} order${property ? ` by property ${property}` : ''}`,
location: baseLocation,
},
];
},
mutuallyExclusive: (
value: string[],
condition: string[],
{ baseLocation }: AssertionFnContext
) => {
if (getIntersectionLength(value, condition) < 2) return [];
return [
{
message: `${condition.join(', ')} keys should be mutually exclusive`,
location: baseLocation.key(),
},
];
},
mutuallyRequired: (
value: string[],
condition: string[],
{ baseLocation }: AssertionFnContext
) => {
const isValid =
getIntersectionLength(value, condition) > 0
? getIntersectionLength(value, condition) === condition.length
: true;
return isValid
? []
: [
{
message: `Properties ${condition.join(', ')} are mutually required`,
location: baseLocation.key(),
},
];
},
requireAny: (value: string[], condition: string[], { baseLocation }: AssertionFnContext) => {
return getIntersectionLength(value, condition) >= 1
? []
: [
{
message: `Should have any of ${condition.join(', ')}`,
location: baseLocation.key(),
},
];
},
ref: (
_value: unknown,
condition: string | boolean,
{ baseLocation, rawValue }: AssertionFnContext
) => {
if (typeof rawValue === 'undefined') return []; // property doesn't exist, no need to lint it with this assert
const hasRef = rawValue.hasOwnProperty('$ref');
if (typeof condition === 'boolean') {
const isValid = condition ? hasRef : !hasRef;
return isValid
? []
: [
{
message: condition ? `should use $ref` : 'should not use $ref',
location: hasRef ? baseLocation : baseLocation.key(),
},
];
}
const regex = regexFromString(condition);
const isValid = hasRef && regex?.test(rawValue['$ref']);
return isValid
? []
: [
{
message: `$ref value should match ${condition}`,
location: hasRef ? baseLocation : baseLocation.key(),
},
];
},
};
export function buildAssertCustomFunction(fn: CustomFunction): AssertionFn {
return (value: string[], options: any, ctx: AssertionFnContext) =>
fn.call(null, value, options, ctx);
}