UNPKG

@cowwoc/requirements

Version:

A fluent API for enforcing design contracts with automatic message generation.

285 lines 10.8 kB
import { Configuration, MessageBuilder, JavascriptValidatorsImpl, ValidationFailureImpl, MultipleFailuresError, assertThatType, isApplicationScope, assertThatInstanceOf, Type, ValidationTarget, IllegalStateError, requireThatStringIsNotEmpty, assertThatValueIsNotNull, ObjectSizeValidatorImpl, messagesIsNotNull, messagesIsInstanceOf, messagesIsNotEqualTo, ValidationFailures, messagesIsEqualTo, messagesIsUndefined, messagesIsNull, messagesIsNotUndefined } from "../internal.mjs"; import isEqual from "lodash.isequal"; /** * Validates the state of a value, recording failures without throwing an error. * * @typeParam T - the type of the value */ class AbstractValidator { static VALUE_IS_UNDEFINED = () => new IllegalStateError("value is invalid"); static CONTAINS_WHITESPACE = /.*\\s.*/u; /** * The application configuration. */ scope; /** * The validator configuration. */ _configuration; /** * The name of the value. */ name; /** * The value being validated. */ value; /** * The contextual information of this validator. */ context; /** * The list of validation failures. */ failures; /** * @param scope - the application configuration * @param configuration - the validator configuration * @param name - the name of the value * @param value - the value being validated * @param context - the contextual information set by a parent validator or the user * @param failures - the list of validation failures * @throws TypeError if `name` is `undefined` or `null` * @throws RangeError if `name` contains whitespace, or is empty * @throws AssertionError if `scope`, `configuration`, `value`, `context` or `failures` are null */ constructor(scope, configuration, name, value, context, failures) { assertThatType(scope, "scope", Type.namedClass("ApplicationScope", () => isApplicationScope(scope))); assertThatInstanceOf(configuration, "configuration", Configuration); requireThatStringIsNotEmpty(name, "name"); if (AbstractValidator.CONTAINS_WHITESPACE.test(name)) { throw new RangeError("name may not contain whitespace.\n" + "actual: \"" + name + "\""); } assertThatValueIsNotNull(value, "value"); assertThatValueIsNotNull(context, "context"); assertThatValueIsNotNull(failures, "failures"); this.scope = scope; this._configuration = configuration; this.name = name; this.value = value; this.context = context; this.failures = failures; } /** * @returns the application configuration */ getScope() { return this.scope; } getName() { return this.name; } validationFailed() { return this.failures.length !== 0; } getValue() { return this.value.orThrow(ObjectSizeValidatorImpl.VALUE_IS_UNDEFINED); } getValueOrDefault(defaultValue) { return this.value.or(defaultValue); } and(validation) { validation(this); return this; } /** * Adds a validation failure and throws an error if the validator is configured to throw an error on * failure. * * @param message - a message that explains what went wrong * @param errorBuilder - creates the error associated with this failure */ addFailure(message, errorBuilder) { const failure = new ValidationFailureImpl(this._configuration, message, errorBuilder); this.failures.push(failure); if (this._configuration.throwOnFailure()) throw failure.getError(); } /** * Adds a `TypeError` validation failure and throws an error if the validator is * configured to throw an error on failure. * * @param message - a message that explains what went wrong */ addTypeError(message) { this.addFailure(message, (theMessage) => new TypeError(theMessage)); } /** * Adds a `RangeError` validation failure and throws an error if the validator is configured * to throw an error on failure. * * @param message - a message that explains what went wrong */ addRangeError(message) { this.addFailure(message, (theMessage) => new RangeError(theMessage)); } configuration() { return this._configuration; } elseGetFailures() { return new ValidationFailures(this.failures); } elseThrow() { const error = this.elseGetError(); if (error === null) return true; throw error; } elseGetError() { if (this.failures.length === 0) return null; if (this.failures.length === 1) return this.failures[0].getError(); return new MultipleFailuresError(this.failures); } getContext() { return new Map(this.context); } withContext(value, name) { this.requireThatNameIsUnique(name, false); if (value === null) this.context.delete(name); else this.context.set(name, value); return this; } getContextAsString() { return new MessageBuilder(this, "").toString(); } /** * Ensures that a name does not conflict with other variable names already in use by the validator. * * @param name - the name of the parameter * @param checkContext - `false` to allow the name to be used even if it conflicts with an * existing name in the validator context * @returns the internal validator of the name * @throws RangeError if `name` is `undefined` or `null` * @throws RangeError if `name`: * <ul> * <li>contains whitespace</li> * <li>is empty</li> * <li>is already in use by the value being validated or the validator context</li> * </ul> */ requireThatNameIsUnique(name, checkContext = true) { const internalValidators = JavascriptValidatorsImpl.INTERNAL; internalValidators.requireThatString(name, "name").isTrimmed().isNotEmpty(); if (AbstractValidator.CONTAINS_WHITESPACE.test(name)) throw new RangeError("name may not contain whitespace"); if (name === this.name) { throw new RangeError(`The name "${name}" is already in use by the value being validated. Choose a different name.`); } if (checkContext && this.context.has(name)) { throw new RangeError(`The name "${name}" is already in use by the validator context. Choose a \ different name.`); } return internalValidators; } isUndefined() { if (!this.value.isUndefined()) { this.addTypeError(messagesIsUndefined(this).toString()); } return this; } isNotUndefined() { if (this.value.isUndefined()) { this.addTypeError(messagesIsUndefined(this).toString()); } return this; } isNull() { if (!this.value.isNull()) { this.addTypeError(messagesIsNull(this).toString()); } return this; } isNotNull() { if (this.value.isNull()) { this.addTypeError(messagesIsNotNull(this).toString()); } return this; } /** * @param otherType - another type * @param mustBeEqual - `true` if the value must match the other type, `false` if it must not match the * other type * @throws TypeError if the value does not match the expected type and the validator is configured to throw * an error on failure * @returns true if the value does not match the expected type */ validateType(otherType, mustBeEqual) { const validationFailed = this.value.map(v => { const typeOfValue = Type.of(v); if (typeof (otherType.typeGuard) !== "undefined") return otherType.typeGuard(v); return isEqual(typeOfValue, otherType) !== mustBeEqual; }).or(true); if (validationFailed) { this.addTypeError(messagesIsInstanceOf(this, otherType).toString()); return false; } return true; } isType(expected) { JavascriptValidatorsImpl.INTERNAL.requireThat(expected, "expected").isNotNull(); if (this.value.map(v => !Type.of(v).equals(expected)).or(true)) { this.addTypeError(messagesIsInstanceOf(this, expected).toString()); } return this; } isInstanceOf(expected) { JavascriptValidatorsImpl.INTERNAL.requireThat(expected, "expected").isNotNull(); const className = Type.of(expected).name; this.validateType(Type.namedClass(className), true); return this; } isNotInstanceOf(expected) { JavascriptValidatorsImpl.INTERNAL.requireThat(expected, "expected").isNotNull(); const className = Type.of(expected).name; this.validateType(Type.namedClass(className), false); return this; } isEqualTo(expected, name) { if (name !== undefined) this.requireThatNameIsUnique(name); if (this.value.map(v => !isEqual(v, expected)).or(true)) { this.addRangeError(messagesIsEqualTo(this, name ?? null, expected).toString()); } return this; } isNotEqualTo(unwanted, name) { if (name !== undefined) this.requireThatNameIsUnique(name); if (this.value.map(v => isEqual(v, unwanted)).or(true)) { this.addRangeError(messagesIsNotEqualTo(this, name ?? null, unwanted).toString()); } return this; } /** * @param name - the name of the value * @param namePrefix - the string to prepend to the name if the name is null * @param value - a value * @param valuePrefix - the string to prepend to the value if the name is null * @returns the prefixed name if it is defined; otherwise, the prefixed string representation of the value */ getNameOrValue(namePrefix, name, valuePrefix, value) { if (name === null) return valuePrefix + this.configuration().stringMappers().toString(value); return namePrefix + MessageBuilder.quoteName(name); } /** * Fails the validation if the value is `undefined` or `null`. */ failOnUndefinedOrNull() { this.value.ifValid(v => { if (v === undefined) this.addRangeError(messagesIsNotUndefined(this).toString()); else if (v === null) this.addRangeError(messagesIsNotNull(this).toString()); }); } } export { AbstractValidator }; //# sourceMappingURL=AbstractValidator.mjs.map