import _ from "lodash"; import {parseInteger} from "../utils/strings"; import Logger from "../logger"; export interface Constraint { type: string, default?: any, optional?: boolean, allowed?: string[], min?: number, max?: number, regex?: RegExp, } export type Constraints = {[key: string]: Constraint}; export type Values = {[key: string]: any}; function isStringArray(arr: any): arr is string[] { return _.isArray(arr) && _.every(arr, (val: any) => _.isString(val)); } export function isConstraint(val: any): val is Constraint { if (!_.isObject(val)) { return false; } const constraint = val as {[key: string]: any}; if (!("type" in constraint) || !_.isString(constraint.type)) { return false; } if ("optional" in constraint && !_.isUndefined(constraint.optional) && !_.isBoolean(constraint.optional)) { return false; } if ("allowed" in constraint && !_.isUndefined(constraint.allowed) && !isStringArray(constraint.allowed)) { return false; } if ("min" in constraint && !_.isUndefined(constraint.min) && !_.isNumber(constraint.min)) { return false; } if ("max" in constraint && !_.isUndefined(constraint.max) && !_.isNumber(constraint.max)) { return false; } // noinspection RedundantIfStatementJS if ("regex" in constraint && !_.isUndefined(constraint.regex) && !_.isRegExp(constraint.regex)) { return false; } return true; } export function isConstraints(constraints: any): constraints is Constraints { if (!_.isObject(constraints)) { return false; } return _.every( constraints, (constraint: any, key: any) => _.isString(key) && isConstraint(constraint) ); } // TODO: sanitize input for further processing as specified by constraints (correct types, trimming, etc.) function isValidBoolean(value: any): boolean { return _.isBoolean(value) || value === 'true' || value === 'false'; } function isValidNumber(constraint: Constraint, value: any): boolean { if (_.isString(value)) { value = parseInteger(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: any): boolean { if (!_.isString(value)) { return false; } return _.indexOf(constraint.allowed, value) >= 0; } function isValidString(constraint: Constraint, value: any): 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: any): 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); } Logger.tag('validation').error('No validation method for constraint type: {}', constraint.type); 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)) { Logger.tag('validation').error('Validation failed: No constraint for field: {}', field); return false; } } return true; } export function forConstraint (constraint: Constraint, acceptUndefined: boolean): (value: any) => boolean { return ((value: any): boolean => isValid(constraint, acceptUndefined, value)); } export function forConstraints (constraints: Constraints, acceptUndefined: boolean): (values: Values) => boolean { return ((values: Values): boolean => areValid(constraints, acceptUndefined, values)); }