UNPKG

@cowwoc/requirements

Version:

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

232 lines 9.7 kB
/* * Copyright (c) 2019 Gili Tzabari * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ import isEqual from "lodash.isequal"; import { Type, assertThatType, Pluralizer, Configuration, AbstractValidator, ValidationTarget, Difference, requireThatValueIsNotNull, objectIsEmpty, objectIsNotEmpty, ObjectSizeValidatorImpl, collectionContains, collectionDoesNotContainExactly, collectionContainsAny, collectionDoesNotContainAny, collectionContainsAll, collectionDoesNotContainAll, collectionDoesNotContainDuplicates, collectionDoesNotContain, collectionContainsExactly } from "../internal.mjs"; /** * Validates the state of a collection. * * @typeParam T - the type the collection * @typeParam E - the type of elements in the array */ class AbstractCollectionValidator extends AbstractValidator { pluralizer; /** * @param scope - the application configuration * @param configuration - the validator configuration * @param name - the name of the value * @param value - the value * @param pluralizer - the type of items in the array * @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, pluralizer, context, failures) { super(scope, configuration, name, value, context, failures); requireThatValueIsNotNull(pluralizer, "pluralizer"); this.pluralizer = pluralizer; } isEmpty() { if (this.value.validationFailed(v => this.getLength(v) === 0)) { this.failOnUndefinedOrNull(); this.addRangeError(objectIsEmpty(this).toString()); } return this; } /** * @param value - the collection * @returns the length of the collection */ getLength(value) { return this.collectionAsArray(value).length; } /** * @param value - the value * @returns the array representation of the value */ collectionAsArray(value) { if (Array.isArray(value)) return value; assertThatType(value, "value", Type.namedClass("Set")); return Array.from(value); } /** * @param value - the value * @returns the array representation of the value */ collectionAsSet(value) { if (value instanceof Set) return value; assertThatType(value, "value", Type.ARRAY); return new Set(value); } isNotEmpty() { if (this.value.validationFailed(v => this.getLength(v) !== 0)) { this.failOnUndefinedOrNull(); this.addRangeError(objectIsNotEmpty(this).toString()); } return this; } contains(expected, name) { if (name !== undefined) this.requireThatNameIsUnique(name); if (this.value.validationFailed(v => this.collectionContainsElement(v, expected))) { this.failOnUndefinedOrNull(); this.addRangeError(collectionContains(this, name ?? null, expected).toString()); } return this; } /** * Indicates if an array contains at least one element of another array. * * @param value - a collection * @param element - an element * @returns true if `value` contains the element */ collectionContainsElement(value, element) { // Set.has(), indexOf(), includes() do not work for multidimensional arrays: // http://stackoverflow.com/a/24943461/14731 const valueAsArray = this.collectionAsArray(value); for (let i = 0; i < valueAsArray.length; ++i) { if (isEqual(valueAsArray[i], element)) return true; } return false; } doesNotContain(unwanted, name) { if (name !== undefined) this.requireThatNameIsUnique(name); if (this.value.validationFailed(v => !this.collectionContainsElement(v, unwanted))) { this.failOnUndefinedOrNull(); this.addRangeError(collectionDoesNotContain(this, name ?? null, unwanted).toString()); } return this; } containsExactly(expected, name) { if (name !== undefined) this.requireThatNameIsUnique(name); const difference = this.value.undefinedOrNullToInvalid(). map(v => Difference.actualVsOther(v, expected)).or(null); if (difference === null || !difference.areTheSame()) { this.failOnUndefinedOrNull(); this.addRangeError(collectionContainsExactly(this, difference, name ?? null, expected, this.pluralizer). toString()); } return this; } doesNotContainExactly(unwanted, name) { if (name !== undefined) this.requireThatNameIsUnique(name); const difference = this.value.undefinedOrNullToInvalid(). map(v => Difference.actualVsOther(v, unwanted)).or(null); if (difference === null || !difference.areDifferent()) { this.failOnUndefinedOrNull(); this.addRangeError(collectionDoesNotContainExactly(this, name ?? null, unwanted, this.pluralizer).toString()); } return this; } containsAny(expected, name) { if (name !== undefined) this.requireThatNameIsUnique(name); if (this.value.validationFailed(v => !this.isDisjoint(this.collectionAsSet(v), this.collectionAsSet(expected)))) { this.failOnUndefinedOrNull(); this.addRangeError(collectionContainsAny(this, name ?? null, expected, this.pluralizer).toString()); } return this; } /** * @param first - a set * @param second - a second set * @returns `true` if the sets do not contain any of the same elements */ isDisjoint(first, second) { // WORKAROUND: Can be replaced by Set.isDisjointFrom() once Typescript supports ES2024 for (const v of first) if (second.has(v)) return false; return true; } doesNotContainAny(unwanted, name) { if (name !== undefined) this.requireThatNameIsUnique(name); const difference = this.value.undefinedOrNullToInvalid(). map(v => Difference.actualVsOther(v, unwanted)).or(null); if (difference === null || !(difference.common.size === 0)) { this.failOnUndefinedOrNull(); this.addRangeError(collectionDoesNotContainAny(this, difference, name ?? null, unwanted, this.pluralizer). toString()); } return this; } containsAll(expected, name) { if (name !== undefined) this.requireThatNameIsUnique(name); const difference = this.value.undefinedOrNullToInvalid(). map(v => Difference.actualVsOther(v, expected)).or(null); if (difference === null || difference.onlyInOther.size !== 0) { this.failOnUndefinedOrNull(); this.addRangeError(collectionContainsAll(this, difference, name ?? null, expected, this.pluralizer). toString()); } return this; } doesNotContainAll(unwanted, name) { if (name !== undefined) this.requireThatNameIsUnique(name); if (this.value.validationFailed(v => !this.collectionContainsAll(v, unwanted))) { this.failOnUndefinedOrNull(); this.addRangeError(collectionDoesNotContainAll(this, name ?? null, unwanted, this.pluralizer). toString()); } return this; } /** * Indicates if an array contains all elements of another array. * * @param value - the value * @param expected - a collection of expected elements * @returns true if `value` contains all the `expected` elements */ collectionContainsAll(value, expected) { // WORKAROUND: Replace with Set.isSupersetOf() once Typescript supports ES2024 for (const element of this.collectionAsArray(expected)) { if (!this.collectionContainsElement(value, element)) return false; } return true; } doesNotContainDuplicates() { const duplicates = this.value.undefinedOrNullToInvalid().map(v => this.getDuplicates(this.collectionAsArray(v))); if (duplicates.validationFailed(v => v.size === 0)) { this.failOnUndefinedOrNull(); this.addRangeError(collectionDoesNotContainDuplicates(this, duplicates.or(null), this.pluralizer). toString()); } return this; } /** * @param value - the value * @returns the duplicate elements in the value */ getDuplicates(value) { const unique = new Set(); const duplicates = new Set(); for (let i = 0; i < value.length; ++i) { const element = value[i]; if (unique.has(element)) duplicates.add(element); else unique.add(element); } return duplicates; } length() { this.failOnUndefinedOrNull(); return new ObjectSizeValidatorImpl(this.scope, this._configuration, this, this.name + ".length()", this.value.undefinedOrNullToInvalid().map(v => this.getLength(v)), this.pluralizer, this.context, this.failures); } } export { AbstractCollectionValidator }; //# sourceMappingURL=AbstractCollectionValidator.mjs.map