/* * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ import { RE2 } from "re2-wasm"; import { findDuplicates } from "../unique"; import { EnumValidationFailure, IntegerEnumValidationFailure, LengthValidationFailure, PatternValidationFailure, RangeValidationFailure, RequiredValidationFailure, UniqueItemsValidationFailure, ValidationFailure, } from "."; export class CompositeValidator implements MultiConstraintValidator { private readonly validators: SingleConstraintValidator[]; constructor(validators: SingleConstraintValidator[]) { this.validators = validators; } validate(input: T | undefined | null, path: string): ValidationFailure[] { const retVal: ValidationFailure[] = []; for (const v of this.validators) { const failure = v.validate(input, path); if (failure) { retVal.push(failure); } } return retVal; } } export class CompositeStructureValidator implements MultiConstraintValidator { private readonly referenceValidator: MultiConstraintValidator; private readonly structureValidator: (input: T, path: string) => ValidationFailure[]; constructor( referenceValidator: MultiConstraintValidator, structureValidator: (input: T, path: string) => ValidationFailure[] ) { this.referenceValidator = referenceValidator; this.structureValidator = structureValidator; } validate(input: T | undefined | null, path: string): ValidationFailure[] { const retVal: ValidationFailure[] = []; retVal.push(...this.referenceValidator.validate(input, path)); if (input !== null && input !== undefined) { retVal.push(...this.structureValidator(input, path)); } return retVal; } } export class CompositeCollectionValidator implements MultiConstraintValidator> { private readonly referenceValidator: MultiConstraintValidator>; private readonly memberValidator: MultiConstraintValidator; constructor(referenceValidator: MultiConstraintValidator>, memberValidator: MultiConstraintValidator) { this.referenceValidator = referenceValidator; this.memberValidator = memberValidator; } validate(input: Iterable | undefined | null, path: string): ValidationFailure[] { const retVal: ValidationFailure[] = []; retVal.push(...this.referenceValidator.validate(input, path)); if (input !== null && input !== undefined) { let i = 0; for (const member of input) { retVal.push(...this.memberValidator.validate(member, `${path}/${i}`)); i += 1; } } return retVal; } } export class CompositeMapValidator implements MultiConstraintValidator> { private readonly referenceValidator: MultiConstraintValidator>; private readonly keyValidator: MultiConstraintValidator; private readonly valueValidator: MultiConstraintValidator; constructor( referenceValidator: MultiConstraintValidator>, keyValidator: MultiConstraintValidator, valueValidator: MultiConstraintValidator ) { this.referenceValidator = referenceValidator; this.keyValidator = keyValidator; this.valueValidator = valueValidator; } validate(input: Record | undefined | null, path: string): ValidationFailure[] { const retVal: ValidationFailure[] = []; retVal.push(...this.referenceValidator.validate(input, path)); if (input !== null && input !== undefined) { Object.keys(input).forEach((key) => { const value = input[key]; retVal.push(...this.keyValidator.validate(key, path)); retVal.push(...this.valueValidator.validate(value, `${path}/${key}`)); }); } return retVal; } } export class NoOpValidator implements MultiConstraintValidator { validate(): ValidationFailure[] { return []; } } export class SensitiveConstraintValidator implements MultiConstraintValidator { private readonly delegate: MultiConstraintValidator; constructor(delegate: MultiConstraintValidator) { this.delegate = delegate; } validate(input: T, path: string): ValidationFailure[] { return this.delegate.validate(input, path).map((f) => { return { ...f, failureValue: null, }; }); } } export interface MultiConstraintValidator { validate(input: T | undefined | null, path: string): ValidationFailure[]; } export interface SingleConstraintValidator { validate(input: T | undefined | null, path: string): F | null; } export class EnumValidator implements SingleConstraintValidator { private readonly allowedValues: string[]; private readonly nonInternalValues: string[]; constructor(allowedValues: readonly string[], nonInternalValues: readonly string[]) { this.allowedValues = allowedValues.slice(); this.nonInternalValues = nonInternalValues.slice(); } validate(input: string | undefined | null, path: string): EnumValidationFailure | null { if (input === null || input === undefined) { return null; } if (this.allowedValues.indexOf(input) < 0) { return { constraintType: "enum", constraintValues: this.nonInternalValues.slice(), path: path, failureValue: input, }; } return null; } } export class IntegerEnumValidator implements SingleConstraintValidator { private readonly allowedValues: number[]; constructor(allowedValues: readonly number[]) { this.allowedValues = allowedValues.slice(); } validate(input: number | undefined | null, path: string): IntegerEnumValidationFailure | null { if (input === null || input === undefined) { return null; } if (this.allowedValues.indexOf(input) < 0) { return { constraintType: "integerEnum", constraintValues: this.allowedValues.slice(), path: path, failureValue: input, }; } return null; } } type LengthCheckable = string | { length: number } | Record; export class LengthValidator implements SingleConstraintValidator { private readonly min?: number; private readonly max?: number; constructor(min?: number, max?: number) { if (min === undefined && max === undefined) { throw new Error("Length constraints must have at least a min or a max."); } this.min = min; this.max = max; } validate(input: LengthCheckable | undefined | null, path: string): LengthValidationFailure | null { if (input === null || input === undefined) { return null; } let length: number; if (typeof input === "string") { length = [...input].length; // string length is defined by the number of code points } else if (LengthValidator.hasLength(input)) { length = input.length; } else { length = Object.keys(input).length; } if ((this.min !== undefined && length < this.min) || (this.max !== undefined && length > this.max)) { return { constraintType: "length", constraintValues: this.min === undefined ? [undefined, this.max!] : this.max === undefined ? [this.min!, undefined] : [this.min!, this.max!], path: path, failureValue: length, }; } return null; } private static hasLength(obj: any): obj is { length: number } { return obj.hasOwnProperty("length"); } } export class RangeValidator implements SingleConstraintValidator { private readonly min?: number; private readonly max?: number; constructor(min?: number, max?: number) { if (min === undefined && max === undefined) { throw new Error("Range constraints must have at least a min or a max."); } this.min = min; this.max = max; } validate(input: number | undefined | null, path: string): RangeValidationFailure | null { if (input === null || input === undefined) { return null; } if ((this.min !== undefined && input < this.min) || (this.max !== undefined && input > this.max)) { return { constraintType: "range", constraintValues: this.min === undefined ? [undefined, this.max!] : this.max === undefined ? [this.min!, undefined] : [this.min!, this.max!], path: path, failureValue: input, }; } return null; } } export class PatternValidator implements SingleConstraintValidator { private readonly inputPattern: string; private readonly pattern: RE2; constructor(pattern: string) { this.inputPattern = pattern; this.pattern = new RE2(pattern, "u"); } validate(input: string | undefined | null, path: string): PatternValidationFailure | null { if (input === null || input === undefined) { return null; } if (!this.pattern.test(input)) { return { constraintType: "pattern", constraintValues: this.inputPattern, failureValue: input, path: path, }; } return null; } } export class RequiredValidator implements SingleConstraintValidator { validate(input: any, path: string): RequiredValidationFailure | null { if (input === null || input === undefined) { return new RequiredValidationFailure(path); } return null; } } export class UniqueItemsValidator implements SingleConstraintValidator, UniqueItemsValidationFailure> { validate(input: Array | undefined | null, path: string): UniqueItemsValidationFailure | null { if (input === null || input === undefined) { return null; } const repeats = findDuplicates(input); if (repeats.length > 0) { return { constraintType: "uniqueItems", path: path, failureValue: [...repeats].sort(), }; } return null; } }