Moved validator to shared code directory.
This commit is contained in:
parent
dda66c67bd
commit
ac0642d8eb
9 changed files with 13 additions and 17 deletions
server/shared
27
server/shared/utils/strings.ts
Normal file
27
server/shared/utils/strings.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import {isString, MAC} from "../types";
|
||||
|
||||
export function normalizeString(str: string): string {
|
||||
return isString(str) ? str.trim().replace(/\s+/g, ' ') : str;
|
||||
}
|
||||
|
||||
export function normalizeMac(mac: MAC): MAC {
|
||||
// parts only contains values at odd indexes
|
||||
const parts = mac.toUpperCase().replace(/[-:]/g, '').split(/([A-F0-9]{2})/);
|
||||
|
||||
const macParts = [];
|
||||
|
||||
for (let i = 1; i < parts.length; i += 2) {
|
||||
macParts.push(parts[i]);
|
||||
}
|
||||
|
||||
return macParts.join(':') as MAC;
|
||||
}
|
||||
|
||||
export function parseInteger(str: string): number {
|
||||
const parsed = parseInt(str, 10);
|
||||
if (parsed.toString() === str) {
|
||||
return parsed;
|
||||
} else {
|
||||
throw new SyntaxError(`String does not represent a valid integer: "${str}"`);
|
||||
}
|
||||
}
|
145
server/shared/validation/validator.ts
Normal file
145
server/shared/validation/validator.ts
Normal file
|
@ -0,0 +1,145 @@
|
|||
import {parseInteger} from "../utils/strings";
|
||||
import {isBoolean, isNumber, isObject, isOptional, isRegExp, isString, toIsArray} from "../types";
|
||||
|
||||
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 };
|
||||
|
||||
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 = 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: 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));
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue