UNPKG

@cowwoc/requirements

Version:

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

300 lines 12.1 kB
import {} from "./internal/internal.mjs"; var TypeCategory; (function (TypeCategory) { TypeCategory[TypeCategory["UNDEFINED"] = 0] = "UNDEFINED"; TypeCategory[TypeCategory["NULL"] = 1] = "NULL"; TypeCategory[TypeCategory["BOOLEAN"] = 2] = "BOOLEAN"; TypeCategory[TypeCategory["NUMBER"] = 3] = "NUMBER"; TypeCategory[TypeCategory["BIGINT"] = 4] = "BIGINT"; TypeCategory[TypeCategory["STRING"] = 5] = "STRING"; TypeCategory[TypeCategory["SYMBOL"] = 6] = "SYMBOL"; TypeCategory[TypeCategory["ARRAY"] = 7] = "ARRAY"; TypeCategory[TypeCategory["FUNCTION"] = 8] = "FUNCTION"; TypeCategory[TypeCategory["CLASS"] = 9] = "CLASS"; })(TypeCategory || (TypeCategory = {})); const FUNCTION_NAME_REGEX = /^function\s+([^(]+)?\(/; const CLASS_NAME_REGEX = /^class\s+([^{\s]+)?.*?{/; const BUILT_IN_CLASS_NAME_REGEX = /^function\s+([^(]+)?\(\) { \[native code] }/; const STARTS_WITH_VOWEL_REGEX = /^[aeiouAEIOU]/; /** * Describes the type of a value. */ class Type { static UNDEFINED = new Type(TypeCategory.UNDEFINED); static NULL = new Type(TypeCategory.NULL); static BOOLEAN = new Type(TypeCategory.BOOLEAN); static NUMBER = new Type(TypeCategory.NUMBER); static BIGINT = new Type(TypeCategory.BIGINT); static STRING = new Type(TypeCategory.STRING); static SYMBOL = new Type(TypeCategory.SYMBOL); static ARRAY = new Type(TypeCategory.ARRAY); /** * An anonymous or arrow function. */ static ANONYMOUS_FUNCTION = new Type(TypeCategory.FUNCTION); category; name; typeGuard; /** * Returns the type of a value. * * @param value - a value * @returns the value's type * @see <a href="http://stackoverflow.com/a/332429/14731">http://stackoverflow.com/a/332429/14731</a> * @see Type.isPrimitive */ static of(value) { const primitive = Type.getPrimitive(value); if (primitive !== null) return primitive; if (Array.isArray(value)) return Type.ARRAY; const valueAsFunction = value; const objectToString = Object.prototype.toString.call(value).slice(8, -1); if (objectToString === "Function") { // A function or a class const valueAsString = valueAsFunction.toString(); const indexOfArrow = valueAsString.indexOf("=>"); const indexOfBody = valueAsString.indexOf("{"); if (indexOfArrow !== -1 && (indexOfBody === -1 || indexOfArrow < indexOfBody)) { // Arrow function return Type.ANONYMOUS_FUNCTION; } const className = CLASS_NAME_REGEX.exec(valueAsString); if (className !== null && className.length >= 2) { // A class const name = className[1]; // Class constructors are returned as named functions return Type.namedClass(name); } const builtInClassName = BUILT_IN_CLASS_NAME_REGEX.exec(valueAsString); if (builtInClassName !== null && builtInClassName.length >= 2) { // A built-in class const name = builtInClassName[1].trim(); // Class constructors are returned as named functions return Type.namedClass(name); } // Anonymous and named functions const functionName = FUNCTION_NAME_REGEX.exec(valueAsString); if (functionName !== null && functionName.length >= 2) { // A named function const name = functionName[1].trim(); return Type.namedFunction(name); } // Anonymous function return Type.ANONYMOUS_FUNCTION; } // Per https://stackoverflow.com/a/30560581/14731 the ES6 specification guarantees the following will // work return Type.namedClass(valueAsFunction.constructor.name); } /** * Returns the type of a named class. * * @param name - the name of the class, or `null` to represent any class. * @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 the type */ static namedClass(name, typeGuard) { return new Type(TypeCategory.CLASS, name, typeGuard); } /** * Returns the type of a named function. * * @param name - (optional) the name of the function. `name` represents any named function. * @returns the type */ static namedFunction(name) { return new Type(TypeCategory.FUNCTION, name); } /** * Returns the type of an `undefined`, `null`, `boolean`, `number`, `bigint`, `string` or `symbol` * value. * * @param value - a value * @returns `null` if the value is not a primitive value */ static getPrimitive(value) { if (value === undefined) return Type.UNDEFINED; if (value === null) return Type.NULL; switch (typeof (value)) { case "boolean": return Type.BOOLEAN; case "number": return Type.NUMBER; case "bigint": return Type.BIGINT; case "string": return Type.STRING; case "symbol": return Type.SYMBOL; } return null; } /** * Creates a new Type. * * @param category - the category of the type * @param name - (optional) the name of the function or class. `null` represents any instance of the type. * @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. * @throws RangeError if neither `type` nor `name` are set. * If `type` does not have a name (e.g. "number" or "array") but `name` is set. */ constructor(category, name = null, typeGuard) { if (!Object.values(TypeCategory).includes(category)) { throw new RangeError(`category must be an instance of TypeCategory. Actual: ${Type.of(category).toString()}`); } this.category = category; this.name = name; this.typeGuard = typeGuard; } /** * @returns `true` if the type is an `undefined`, `null`, `boolean`, `number`, `bigint`, `string` or * `symbol` value */ isPrimitive() { switch (this.category) { case TypeCategory.UNDEFINED: case TypeCategory.NULL: case TypeCategory.BOOLEAN: case TypeCategory.NUMBER: case TypeCategory.BIGINT: case TypeCategory.STRING: case TypeCategory.SYMBOL: return true; default: return false; } } /** * Indicates if this type is equal to another type. * * @param other - another type * @returns true if this type matches `other` */ equals(other) { return other.category === this.category && (other.name === this.name || this.name === null || other.name === null); } /** * Returns the type of this type. * * @returns the type of this type */ getTypeOf() { switch (this.category) { case TypeCategory.UNDEFINED: case TypeCategory.NULL: case TypeCategory.BOOLEAN: case TypeCategory.NUMBER: case TypeCategory.BIGINT: case TypeCategory.STRING: case TypeCategory.SYMBOL: case TypeCategory.ARRAY: case TypeCategory.FUNCTION: return this; case TypeCategory.CLASS: return Type.namedClass(this.name); } } /** * Indicates whether this type is a subtype of another type. Note that types are considered subtypes of * themselves. * * @param parent - the parent type * @returns * <ul> * <li>`true` if `child` extends `parent`</li> * <li>`false` if `parent` or `child` are `undefined`, `null` or an object</li> * <li>`false` if `child` does not extend `parent`</li> * </ul> */ isSubtypeOf(parent) { // 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`. switch (this.category) { case TypeCategory.UNDEFINED: case TypeCategory.NULL: return false; case TypeCategory.BOOLEAN: case TypeCategory.NUMBER: case TypeCategory.BIGINT: case TypeCategory.STRING: case TypeCategory.SYMBOL: case TypeCategory.ARRAY: case TypeCategory.FUNCTION: return this.equals(parent); case TypeCategory.CLASS: { if (parent === Type.UNDEFINED || parent === Type.NULL) return false; if (parent.name === null) { // null represents any class return true; } // There is no way to provide type-casting for a dynamic lookup of an unknown type /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ const parentClass = globalThis[parent.name]; /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ if (this.name == null) { // null represents any class return true; } /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ const childClass = globalThis[this.name]; /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */ // https://stackoverflow.com/a/14486171/14731 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return childClass.prototype instanceof parentClass; } } } /** * @returns the string representation of this object */ toString() { switch (this.category) { case TypeCategory.UNDEFINED: return "undefined"; case TypeCategory.NULL: return "null"; case TypeCategory.BOOLEAN: return "a boolean"; case TypeCategory.NUMBER: return "a number"; case TypeCategory.BIGINT: return "a bigint"; case TypeCategory.STRING: return "a string"; case TypeCategory.SYMBOL: return "a symbol"; case TypeCategory.ARRAY: return "an array"; case TypeCategory.FUNCTION: { if (this.name === null) return "a function"; return `a function named ${this.name}`; } case TypeCategory.CLASS: { if (this.name === null) return "an object"; if (STARTS_WITH_VOWEL_REGEX.test(this.name)) return `an ${this.name}`; return `a ${this.name}`; } } } } export { Type, TypeCategory }; //# sourceMappingURL=Type.mjs.map