UNPKG

@cowwoc/requirements

Version:

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

447 lines 15.7 kB
import { Type, TypeCategory, AssertionError } from "../internal.mjs"; import isEqual from "lodash.isequal"; /** * Indicates if an object is an instance of a type. To convert a type to an object, use * `prototype` such as `Error.prototype`. To convert an object to a type, use * `constructor` such as `instance.constructor`. * * @param child - the child class * @param parent - the parent class * @returns `true` if `child` extends `parent`; false if * `parent` or `child` are `undefined` or `null`; false if `child` does not extend `parent` */ function classExtends(child, parent) { // https://stackoverflow.com/a/14486171/14731 return child.prototype instanceof parent; } /** * Throws an `Error` if `condition` is false. * * @param condition - a condition * @param error - the type of error to throw (Default: `Error`) * @param message - the error message to use on failure * @throws AssertionError if `condition` is false */ function assert(condition, error = (message) => new AssertionError(message), message) { // Will be stripped out using uglify.js option "pure_funcs" if (!condition) throw error(message); } /** * Ensures that an object is defined. * * @param value - the value of a parameter * @param name - the name of the parameter * @returns `true` * @throws TypeError if: * <ul> * <li>`name` is not a string</li> * <li>if `value` is `undefined`</li> * </ul> */ function requireThatValueIsDefined(value, name) { const type = Type.of(name); if (type !== Type.STRING) { throw new TypeError(`name must be a string. Actual: ${internalValueToString(name)} Type : ${internalValueToString(type)}`); } if (value === undefined) throw new TypeError(name + " must be defined"); return true; } /** * Ensures that an object is defined and not null. * * @param value - the value of a parameter * @param name - the name of the parameter * @returns `true` * @throws TypeError if: * <ul> * <li>`name` is not a string</li> * <li>if `value` is `undefined` or `null`</li> * </ul> */ function requireThatValueIsNotNull(value, name) { requireThatValueIsDefined(value, name); if (value === null) throw new TypeError(name + " may not be null"); return true; } /** * Ensures that an object is defined and not null. * * @param value - the value of a parameter * @param name - the name of the parameter * @returns `true` * @throws AssertionError if: * <ul> * <li>`name` is not a string</li> * <li>if `value` is `undefined` or `null`</li> * </ul> */ function assertThatValueIsNotNull(value, name) { try { assert(requireThatValueIsNotNull(value, name)); } catch (e) { if (e instanceof Error) { const assertionError = new AssertionError(e.message); assertionError.stack = e.stack?.replace(e.name, assertionError.name); throw assertionError; } throw e; } } /** * Requires that an object has the expected type. * * @param value - the value of a parameter * @param name - the name of the parameter * @param type - `value`'s expected type * @returns `true` * @throws TypeError if `value` does not have the expected `type`. If `name` is not a string. */ function requireThatType(value, name, type) { const typeOfName = Type.of(name); if (typeOfName !== Type.STRING) { throw new TypeError(`name must be a string. Actual: ${internalValueToString(name)} Type : ${typeOfName.toString()}`); } const typeOfValue = Type.of(value); let matchFound; if (typeof (type.typeGuard) !== "undefined") matchFound = type.typeGuard(value); else matchFound = isEqual(typeOfValue, type); if (!matchFound) { throw new TypeError(`${name} must be a ${internalValueToString(type)}. Actual: ${internalValueToString(value)} Type : ${typeOfValue.toString()}`); } return true; } /** * Requires that a value has the expected type if assertions are enabled. We assume that * `assert()` will be stripped out at build-time if assertions are disabled. * * @param value - the value of a parameter * @param name - the name of the parameter * @param type - `value`'s expected type * @returns `true` * @throws TypeError if `value` does not have the expected `type`. If `name` is not a string. */ function assertThatType(value, name, type) { try { assert(requireThatType(value, name, type)); } catch (e) { if (e instanceof Error) { const assertionError = new AssertionError(e.message); assertionError.stack = e.stack?.replace(e.name, assertionError.name); throw assertionError; } throw e; } } /** * Requires that an object has the expected type category. * * @param value - the value of a parameter * @param name - the name of the parameter * @param typeCategory - `value`'s expected type category * @param typeGuard - (optional) for certain types, such as Typescript interfaces, runtime validation is not * possible. In such a case, use a type guard to check if the value satisfies the type condition. * @returns `true` * @throws TypeError if `value` does not have the expected `typeCategory`. If `name` is not a string. */ function requireThatTypeCategory(value, name, typeCategory, typeGuard) { const typeOfName = Type.of(name); if (typeOfName !== Type.STRING) { throw new TypeError(`name must be a string. Actual: ${internalValueToString(name)} Type : ${typeOfName.toString()}`); } const typeCategoryOfValue = Type.of(value).category; let matchFound; if (typeGuard !== undefined) matchFound = typeGuard(value); else matchFound = isEqual(typeCategoryOfValue, typeCategory); if (!matchFound) { throw new TypeError(`${name} must be a ${TypeCategory[typeCategory]}. Actual : ${internalValueToString(value)} TypeCategory: ${TypeCategory[typeCategoryOfValue]}`); } return true; } /** * Requires that a value has the expected type category if assertions are enabled. We assume that * `assert()` will be stripped out at build-time if assertions are disabled. * * @param value - the value of a parameter * @param name - the name of the parameter * @param category - `value`'s expected type category * @param typeGuard - (optional) for certain types, such as Typescript interfaces, runtime validation is not * possible. In such a case, use a type guard to check if the value satisfies the type condition. * @returns `true` * @throws TypeError if `value` does not have the expected `typeCategory` category. If `name` is not a string. */ function assertThatTypeCategory(value, name, category, typeGuard) { try { assert(requireThatTypeCategory(value, name, category, typeGuard)); } catch (e) { if (e instanceof Error) { const assertionError = new AssertionError(e.message); assertionError.stack = e.stack?.replace(e.name, assertionError.name); throw assertionError; } throw e; } } /** * Requires that an object is an instance of `type`. * * @param value - the value of a parameter * @param name - the name of the parameter * @param type - the class that `value` is expected to be an instance of. This may not reference * an interface or abstract class because * <a href="https://stackoverflow.com/a/47082428/14731">Typescript does not expose them at runtime</a>. * @returns `true` * @throws TypeError if `value` is not an instance of `type`. * If `name` is not a string. */ function requireThatInstanceOf(value, name, // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style type) { // WARNING: Per https://github.com/typescript-eslint/typescript-eslint/issues/9370 instanceof returns false // if a class "implements" an interface or another class. const typeOfName = Type.of(name); if (typeOfName !== Type.STRING) { throw new TypeError(`name must be a string. Actual : ${internalValueToString(name)} Actual.type: ${typeOfName.toString()}`); } const typeOfType = Type.of(type); switch (typeOfType.category) { case TypeCategory.CLASS: { const classType = type; if (!(value instanceof classType)) { const typeOfValue = Type.of(value); throw new TypeError(`${name} must be ${typeOfType.toString()}. Actual: ${typeOfValue.toString()}`); } break; } case TypeCategory.NUMBER: case TypeCategory.STRING: { // Enum if (!Object.values(type).includes(value)) { throw new TypeError(`${name} must be ${typeOfType.toString()}. Actual: ${internalValueToString(type)} Type : ${typeOfType.toString()}`); } break; } default: throw new TypeError(`type must be a class or enum. Actual: ${internalValueToString(typeOfType)}`); } return true; } /** * Requires that an object is an instance of the expected type. * * @param value - the value of a parameter * @param name - the name of the parameter * @param type - the class the value is expected to be an instance of * @throws TypeError if `value` is not an instance of `type`. * If `name` is not a string. */ function assertThatInstanceOf(value, name, type) { try { assert(requireThatInstanceOf(value, name, type)); } catch (e) { if (e instanceof Error) { const assertionError = new AssertionError(e.message); assertionError.stack = e.stack?.replace(e.name, assertionError.name); throw assertionError; } throw e; } } /** * Requires that a string is not empty. * * @param value - the value of a parameter * @param name - the name of the parameter * @returns `true` * @throws TypeError if `name` or `value` are empty. * If `name` is not a string. */ function requireThatStringIsNotEmpty(value, name) { requireThatType(name, "name", Type.STRING); name = name.trim(); if (name.length === 0) throw new RangeError("name may not be empty"); requireThatType(value, "value", Type.STRING); value = value.trim(); if (value.length === 0) throw new RangeError(`${name} may not be empty`); return true; } /** * Requires that a string is not empty. * * @param value - the value of a parameter * @param name - the name of the parameter * @returns `true` * @throws TypeError if `name` or `value` are empty. * If `name` is not a string. */ function assertThatStringIsNotEmpty(value, name) { try { assert(requireThatStringIsNotEmpty(value, name)); } catch (e) { if (e instanceof Error) { const assertionError = new AssertionError(e.message); assertionError.stack = e.stack?.replace(e.name, assertionError.name); throw assertionError; } throw e; } } /** * Converts an internal value to a string. * * @param value - a value * @returns the string representation of the value */ function internalValueToString(value) { let typeOfObject = Type.of(value); switch (typeOfObject.category) { case TypeCategory.CLASS: { switch (typeOfObject.name) { case "Set": { const set = value; return arrayToString(Array.from(set.values())); } case "Map": { const result = {}; const map = value; for (const entry of map.entries()) { const key = internalValueToString(entry[0]); result[key] = entry[1]; } return JSON.stringify(result, null, 2); } } break; } case TypeCategory.UNDEFINED: return "undefined"; case TypeCategory.NULL: return "null"; case TypeCategory.STRING: return quoteString(value); default: return JSON.stringify(value, undefined, 2); } // An instance of a user class let current = value; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (true) { // See http://stackoverflow.com/a/22445303/14731, // Invoke toString() if it was defined // https://stackoverflow.com/a/57214796/14731: invoke toString() on safeTypes if (Object.prototype.hasOwnProperty.call(current.constructor.prototype, "toString")) return current.toString(); // Get the superclass and try again const superclass = getSuperclass(current); let className; if (superclass === null) className = "Object"; else { current = superclass; typeOfObject = Type.of(current); assert(typeOfObject.category === TypeCategory.CLASS, undefined, `expected: CLASS actual: ${typeOfObject.toString()}`); className = typeOfObject.name; } if (className === "Object") { // Prefer JSON.stringify() to Object.toString(). return JSON.stringify(value, null, 2); } } } /** * Quotes a String, escaping any nested quotes. * * @param value - a `String` * @returns the quoted string */ function quoteString(value) { let result = ""; for (let i = 0; i < value.length; ++i) { const char = value.charAt(i); if (char == "\"") result += "\\\""; else result += char; } result = "\"" + result; result = result + "\""; return result.toString(); } /** * Returns the superclass of a value's type. * * @param value - a value * @returns `null` if the type does not have a superclass */ function getSuperclass(value) { return Object.getPrototypeOf(value.constructor.prototype); } /** * @param array - an array * @returns the string representation of the array, using toString() to convert nested values */ function arrayToString(array) { let result = "["; // Can't use Array.join() because it doesn't handle nested arrays well const size = array.length; for (let i = 0; i < size; ++i) { result += internalValueToString(array[i]); if (i < size - 1) result += ", "; } result += "]"; return result; } /** * @param value - a name * @param name - the name of the name variable * @throws TypeError if `name` or `value` are not a string * @throws RangeError if `value` is empty */ function verifyName(value, name) { requireThatType(name, "name", Type.STRING); requireThatType(value, "value", Type.STRING); const trimmed = value.trim(); if (value.length !== trimmed.length) throw new RangeError(`${name} may not contain leading or trailing whitespace. Actual: "${name}"`); if (trimmed.length === 0) throw new RangeError(`${name} may not be empty`); } export { classExtends, assert, requireThatValueIsDefined, requireThatValueIsNotNull, assertThatValueIsNotNull, requireThatType, assertThatType, requireThatTypeCategory, assertThatTypeCategory, requireThatInstanceOf, assertThatInstanceOf, requireThatStringIsNotEmpty, assertThatStringIsNotEmpty, internalValueToString, getSuperclass, verifyName, quoteString }; //# sourceMappingURL=Objects.mjs.map