181 lines
4.1 KiB
TypeScript
181 lines
4.1 KiB
TypeScript
import { parseToInteger } from "../utils/numbers";
|
|
import {
|
|
isBoolean,
|
|
isNumber,
|
|
isObject,
|
|
isOptional,
|
|
isRegExp,
|
|
isString,
|
|
toIsArray,
|
|
} from "../types";
|
|
|
|
export interface Constraint {
|
|
type: string;
|
|
|
|
default?: unknown;
|
|
|
|
optional?: boolean;
|
|
|
|
allowed?: string[];
|
|
|
|
min?: number;
|
|
max?: number;
|
|
|
|
regex?: RegExp;
|
|
}
|
|
|
|
export type Constraints = { [key: string]: Constraint };
|
|
export type NestedConstraints = {
|
|
[key: string]: Constraint | Constraints | NestedConstraints;
|
|
};
|
|
export type Values = { [key: string]: unknown };
|
|
|
|
export function isConstraint(arg: unknown): arg is Constraint {
|
|
if (!isObject(arg)) {
|
|
return false;
|
|
}
|
|
|
|
const constraint = arg as Constraint;
|
|
return (
|
|
isString(constraint.type) &&
|
|
// default?: any
|
|
isOptional(constraint.optional, isBoolean) &&
|
|
isOptional(constraint.allowed, toIsArray(isString)) &&
|
|
isOptional(constraint.min, isNumber) &&
|
|
isOptional(constraint.max, isNumber) &&
|
|
isOptional(constraint.regex, isRegExp)
|
|
);
|
|
}
|
|
|
|
export function isConstraints(
|
|
constraints: unknown
|
|
): constraints is Constraints {
|
|
if (!isObject(constraints)) {
|
|
return false;
|
|
}
|
|
|
|
return Object.entries(constraints).every(
|
|
([key, constraint]) => isString(key) && isConstraint(constraint)
|
|
);
|
|
}
|
|
|
|
// TODO: sanitize input for further processing as specified by constraints (correct types, trimming, etc.)
|
|
|
|
function isValidBoolean(value: unknown): boolean {
|
|
return isBoolean(value) || value === "true" || value === "false";
|
|
}
|
|
|
|
function isValidNumber(constraint: Constraint, value: unknown): boolean {
|
|
if (isString(value)) {
|
|
value = parseToInteger(value);
|
|
}
|
|
|
|
if (!isNumber(value)) {
|
|
return false;
|
|
}
|
|
|
|
if (isNaN(value) || !isFinite(value)) {
|
|
return false;
|
|
}
|
|
|
|
if (isNumber(constraint.min) && value < constraint.min) {
|
|
return false;
|
|
}
|
|
|
|
// noinspection RedundantIfStatementJS
|
|
if (isNumber(constraint.max) && value > constraint.max) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function isValidEnum(constraint: Constraint, value: unknown): boolean {
|
|
if (!isString(value)) {
|
|
return false;
|
|
}
|
|
|
|
const allowed = constraint.allowed || [];
|
|
return allowed.indexOf(value) >= 0;
|
|
}
|
|
|
|
function isValidString(constraint: Constraint, value: unknown): boolean {
|
|
if (!constraint.regex) {
|
|
throw new Error(
|
|
"String constraints must have regex set: " + constraint
|
|
);
|
|
}
|
|
|
|
if (!isString(value)) {
|
|
return false;
|
|
}
|
|
|
|
const trimmed = value.trim();
|
|
return (
|
|
(trimmed === "" && constraint.optional) ||
|
|
constraint.regex.test(trimmed)
|
|
);
|
|
}
|
|
|
|
function isValid(
|
|
constraint: Constraint,
|
|
acceptUndefined: boolean,
|
|
value: unknown
|
|
): boolean {
|
|
if (value === undefined) {
|
|
return acceptUndefined || constraint.optional === true;
|
|
}
|
|
|
|
switch (constraint.type) {
|
|
case "boolean":
|
|
return isValidBoolean(value);
|
|
|
|
case "number":
|
|
return isValidNumber(constraint, value);
|
|
|
|
case "enum":
|
|
return isValidEnum(constraint, value);
|
|
|
|
case "string":
|
|
return isValidString(constraint, value);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function areValid(
|
|
constraints: Constraints,
|
|
acceptUndefined: boolean,
|
|
values: Values
|
|
): boolean {
|
|
const fields = new Set(Object.keys(constraints));
|
|
for (const field of fields) {
|
|
if (!isValid(constraints[field], acceptUndefined, values[field])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (const field of Object.keys(values)) {
|
|
if (!fields.has(field)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function forConstraint(
|
|
constraint: Constraint,
|
|
acceptUndefined: boolean
|
|
): (value: unknown) => boolean {
|
|
return (value: unknown): boolean =>
|
|
isValid(constraint, acceptUndefined, value);
|
|
}
|
|
|
|
export function forConstraints(
|
|
constraints: Constraints,
|
|
acceptUndefined: boolean
|
|
): (values: Values) => boolean {
|
|
return (values: Values): boolean =>
|
|
areValid(constraints, acceptUndefined, values);
|
|
}
|