@cowwoc/requirements
Version:
A fluent API for enforcing design contracts with automatic message generation.
300 lines • 12.1 kB
JavaScript
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