@aedart/support
Version:
The Ion support package
521 lines (495 loc) • 17.5 kB
JavaScript
/**
* @aedart/support
*
* BSD-3-Clause, Copyright (c) 2023-present Alin Eugen Deac <aedart@gmail.com>.
*/
;
var misc = require('@aedart/support/misc');
var arrays = require('@aedart/support/arrays');
var reflections = require('@aedart/contracts/support/reflections');
var objects = require('@aedart/support/objects');
var objects$1 = require('@aedart/contracts/support/objects');
/**
* Determine if target object has a prototype property defined
*
* **Warning**: _This method is NOT the same as checking if {@link Reflect.getPrototypeOf} of an object is `null`!_
* _The method literally checks if a "prototype" property is defined in target, that it is not `null` or `undefined`,
* and that its of the [type]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof} 'object'!_
*
* **Note**: _Method returns `false` if `null` given as argument!_
*
* @param {object} target
*
* @returns {boolean}
*/
function hasPrototypeProperty(target) {
return misc.isset(target)
&& typeof target['prototype'] == 'object'
&& target['prototype'] !== null;
}
/**
* Assert that given target object has a "prototype" property defined
*
* @see hasPrototypeProperty
*
* @param {object} target
* @param {string} [message]
*
* @throws {TypeError} If target object does not have "prototype" property
*/
function assertHasPrototypeProperty(target, message = 'target object has no "prototype" property') {
if (!hasPrototypeProperty(target)) {
throw new TypeError(message);
}
}
/**
* Returns the parent class of given target class
*
* **Note**: _If target has a parent that matches
* [FUNCTION_PROTOTYPE]{@link import('@aedart/contracts/support/reflections').FUNCTION_PROTOTYPE}, then `null` is returned!_
*
* @param {ConstructorLike} target The target class
*
* @returns {ConstructorLike | null} Parent class or `null`, if target has no parent class.
*
* @throws {TypeError}
*/
function getParentOfClass(target) {
if (!misc.isset(target)) {
throw new TypeError('getParentOfClass() expects a target class as argument, undefined given');
}
const parent = Reflect.getPrototypeOf(target);
if (parent === reflections.FUNCTION_PROTOTYPE) {
return null;
}
return parent;
}
/**
* Returns all parent classes of given target
*
* @see {getParentOfClass}
*
* @param {ConstructorLike} target The target class.
* @param {boolean} [includeTarget=false] If `true`, then given target is included in the output as the first element.
*
* @returns {ConstructorLike[]} List of parent classes, ordered by the top-most parent class first.
*
* @throws {TypeError}
*/
function getAllParentsOfClass(target, includeTarget = false) {
if (!misc.isset(target)) {
throw new TypeError('getAllParentsOfClass() expects a target class as argument, undefined given');
}
const output = [];
if (includeTarget) {
output.push(target);
}
let parent = getParentOfClass(target);
while (parent !== null) {
output.push(parent);
parent = getParentOfClass(parent);
}
return output;
}
/**
* Returns property keys that are defined target's prototype
*
* @param {ConstructorLike} target
* @param {boolean} [recursive=false] If `true`, then target's parent prototypes are traversed and all
* property keys are returned.
*
* @returns {PropertyKey[]}
*
* @throws {TypeError} If target object does not have "prototype" property
*/
function classOwnKeys(target, recursive = false) {
assertHasPrototypeProperty(target);
if (!recursive) {
return Reflect.ownKeys(target.prototype);
}
// Obtain target's parent classes...
const parents = getAllParentsOfClass(target, true).reverse();
const ownKeys = new Set();
for (const parent of parents) {
const keys = Reflect.ownKeys(parent.prototype);
for (const key of keys) {
ownKeys.add(key);
}
}
return Array.from(ownKeys);
}
/**
* Determine if target class look like given blueprint.
*
* @param {object} target
* @param {ClassBlueprint} blueprint
*
* @throws {TypeError} If target object does not have "prototype" property. Or, if blueprint does not contain at least
* one member or static member.
*/
function classLooksLike(target, blueprint) {
if (!hasPrototypeProperty(target)) {
return false;
}
// Abort if both blueprint does not define either members or static members property
const hasBlueprintStaticMembers = Reflect.has(blueprint, 'staticMembers');
const hasBlueprintMembers = Reflect.has(blueprint, 'members');
if (!hasBlueprintStaticMembers && !hasBlueprintMembers) {
throw new TypeError('Blueprint must at least have a "members" or "staticMembers" property defined');
}
// Abort if both members and static members properties are empty
const amountStaticMembers = blueprint?.staticMembers?.length || 0;
const amountMembers = blueprint?.members?.length || 0;
// Check for static members
let hasAllStaticMembers = false;
if (amountStaticMembers > 0) {
for (const staticMember of blueprint.staticMembers) {
if (!Reflect.has(target, staticMember)) {
return false;
}
}
hasAllStaticMembers = true;
}
// Check for members
if (amountMembers > 0) {
// We can return here, because static members have been checked and code aborted if a member
// was missing...
return arrays.includesAll(classOwnKeys(target, true), blueprint.members);
}
// Otherwise, if there were any static members and all a present in target, then
// check passes.
return amountStaticMembers > 0 && hasAllStaticMembers;
}
/**
* Returns a {@link PropertyDescriptor} object, from target's prototype that matches given property key
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getOwnPropertyDescriptor
*
* @param {ConstructorLike} target Class that contains property in its prototype
* @param {PropertyKey} key Name of the property
*
* @return {PropertyDescriptor|undefined} Property descriptor or `undefined` if property does
* not exist in target's prototype.
*
* @throws {TypeError} If target is not an object or has no prototype
*/
function getClassPropertyDescriptor(target, key) {
assertHasPrototypeProperty(target);
return Reflect.getOwnPropertyDescriptor(target.prototype, key);
}
/**
* Returns all property descriptors that are defined target's prototype
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getOwnPropertyDescriptor
*
* @param {ConstructorLike} target The target class
* @param {boolean} [recursive=false] If `true`, then target's parent prototypes are traversed.
* Descriptors are merged, such that the top-most class' descriptors
* are returned.
*
* @return {Record<PropertyKey, PropertyDescriptor>} Object with the property descriptors, or empty object of target has
* properties defined.
*
* @throws {TypeError} If target is not an object or has no prototype property
*/
function getClassPropertyDescriptors(target, recursive = false) {
assertHasPrototypeProperty(target);
// Define list of targets...
let targets = [target.prototype];
// Obtain target's parent classes, such that top-most descriptors can be returned, if needed.
if (recursive) {
targets = getAllParentsOfClass(target.prototype, true).reverse();
}
const output = Object.create(null);
// Obtain property descriptors for all targets
for (const t of targets) {
const keys = Reflect.ownKeys(t);
for (const key of keys) {
const descriptor = getClassPropertyDescriptor(t.constructor, key);
// If for some reason we are unable to obtain a descriptor, then skip it.
if (descriptor === undefined) {
continue;
}
// Merge evt. existing descriptor object with the one obtained from target.
if (Reflect.has(output, key)) {
output[key] = objects.merge()
.using({ overwriteWithUndefined: false })
.of(output[key], descriptor);
continue;
}
output[key] = descriptor;
}
}
return output;
}
/**
* Returns target class' constructor name, if available
*
* @param {ConstructorLike} target
* @param {string|null} [defaultValue=null] A default string value to return if target has no constructor name
*
* @return {string|null} Constructor name, or default value
*/
function getConstructorName(target, defaultValue = null) {
if (!objects.isset(target, ['prototype', 'constructor', 'name'])) {
return defaultValue;
}
const name = target.prototype.constructor.name;
return name.length > 0
? name
: defaultValue;
}
/**
* Return target class' constructor name or default to target's description tag if a name is unavailable
*
* **Note**: _Method is a shortcut for the following:_
* ```js
* getConstructorName(target, descTag(target));
* ```
*
* @see getConstructorName
* @see descTag
*
* @param {ConstructorLike} target
*
* @return {string}
*/
function getNameOrDesc(target) {
return getConstructorName(target, misc.descTag(target));
}
/**
* Determine if given property key is a method in target
*
* @param {object} target
* @param {PropertyKey} property
*
* @return {boolean}
*/
function isMethod(target, property) {
return typeof target == 'object'
&& target !== null
&& Reflect.has(target, property)
&& typeof target[property] == 'function';
}
/**
* Determine if given target object contains all given methods
*
* @param {object} target
* @param {...PropertyKey} [methods]
*
* @return {boolean}
*/
function hasAllMethods(target, ...methods) {
if (!misc.isset(target) || typeof target != 'object' || Array.isArray(target) || methods.length === 0) {
return false;
}
for (const method of methods) {
if (!isMethod(target, method)) {
return false;
}
}
return true;
}
/**
* Determine if given target object contains method
*
* @param {object} target
* @param {PropertyKey} method
*
* @return {boolean}
*/
function hasMethod(target, method) {
return hasAllMethods(target, method);
}
/**
* Determine if given value is a class constructor (es6 style)
*
* @see https://github.com/caitp/TC39-Proposals/blob/trunk/tc39-reflect-isconstructor-iscallable.md
*
* @param {unknown} value
*
* @return {boolean}
*/
function isClassConstructor(value) {
// Source is heavily inspired by Denis Pushkarev's Core-js implementation of
// Function.isCallable / Function.isConstructor, License MIT
// @see https://github.com/zloirock/core-js#function-iscallable-isconstructor-
if (typeof value != 'function') {
return false;
}
try {
// Obtain a small part of the argument's string representation, to avoid
// too large string from being processed by regex.
const source = value.toString().slice(0, 25);
// Determine if source starts with "class".
return new RegExp(/^\s*class\b/).test(source);
}
catch (e) {
return false;
}
}
/**
* Determine if given argument is callable, but is not a class constructor (es6 style)
*
* @see {isClassConstructor}
* @see https://github.com/caitp/TC39-Proposals/blob/trunk/tc39-reflect-isconstructor-iscallable.md
*
* @param {unknown} value
*
* @return {boolean}
*/
function isCallable(value) {
// Source is heavily inspired by Denis Pushkarev's Core-js implementation of
// Function.isCallable / Function.isConstructor, License MIT
// @see https://github.com/zloirock/core-js#function-iscallable-isconstructor-
return typeof value == 'function' && !isClassConstructor(value);
}
/**
* Determine if value is a [Class Method Reference]{@link import('@aedart/constracts').ClassMethodReference}
*
* @param {unknown} value
*
* @return {boolean}
*/
function isClassMethodReference(value) {
if (!Array.isArray(value) || value.length != 2) {
return false;
}
const targetType = typeof value[0];
// If target appears to be a class constructor...
if (targetType == 'function' && hasPrototypeProperty(value[0])) {
// Method must exist in class' prototype
return isMethod(value[0].prototype, value[1]);
}
// If target is an object (class instance)
if (targetType == 'object') {
// Method must exist in class instance
return isMethod(value[0], value[1]);
}
// Otherwise target is not valid
return false;
}
/**
* Determine if given argument is a constructor
*
* @see https://github.com/caitp/TC39-Proposals/blob/trunk/tc39-reflect-isconstructor-iscallable.md
*
* @param {unknown} argument
*
* @return {boolean}
*/
function isConstructor(argument) {
// Source is heavily inspired by Denis Pushkarev's Core-js implementation of
// Function.isCallable / Function.isConstructor, License MIT
// @see https://github.com/zloirock/core-js#function-iscallable-isconstructor-
if (typeof argument != 'function') {
return false;
}
try {
// Attempt to construct a new function, using the argument as it's newTarget.
// If the newTarget isn't a constructor, a TypeError will be thrown. The newTarget's
// original constructor (if it has one) is not invoked, using this technique...
// @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct#syntax
// @see https://tc39.es/ecma262/multipage/reflection.html#sec-reflect.construct
Reflect.construct(function () { }, [], argument);
// If we reach this point, it means that the argument is a constructor.
return true;
}
catch (e) {
// Regardless of the error cause, we assume that the argument is not a constructor...
return false;
}
}
/**
* Determine if property key is unsafe
*
* @see DANGEROUS_PROPERTIES
*
* @param {PropertyKey} key
*
* @returns {boolean}
*/
function isKeyUnsafe(key) {
return objects$1.DANGEROUS_PROPERTIES.includes(key);
}
/**
* Opposite of {@link isKeyUnsafe}
*
* @param {PropertyKey} key
*
* @returns {boolean}
*/
function isKeySafe(key) {
return !isKeyUnsafe(key);
}
/**
* Determine if target class is a subclass (_child class_) of given superclass (_parent class_)
*
* **Note**: _Method determines if target is a child of given superclass, by checking if the `target.prototype`
* is an instance of given superclass (`target.prototype instanceof superclass`)
* However, if given target or superclass does not have a prototype property, then `false` is returned._
*
* @param {object} target
* @param {ConstructorLike} superclass
*
* @returns {boolean} `true` if target is a subclass of given superclass, `false` otherwise.
*/
function isSubclass(target, superclass) {
if (!hasPrototypeProperty(target) || !hasPrototypeProperty(superclass) || target === superclass) {
return false;
}
return target.prototype instanceof superclass;
}
/**
* Determine if target class is a subclass of given superclass, or if it looks like given blueprint
*
* **Note**: _Method is an alias for `isSubclass(target, superclass) || classLooksLike(target, blueprint)`._
*
* @see isSubclass
* @see classLooksLike
*
* @param {object} target
* @param {ConstructorLike} superclass
* @param {ClassBlueprint} blueprint
*
* @throws {TypeError}
*/
function isSubclassOrLooksLike(target, superclass, blueprint) {
return isSubclass(target, superclass) || classLooksLike(target, blueprint);
}
/**
* Determine if object of a "weak" kind, e.g. `WeakRef`, `WeakMap` or `WeakSet`
*
* @param {object} value
*
* @return {boolean}
*/
function isWeakKind(value) {
return value && (value instanceof WeakRef
|| value instanceof WeakMap
|| value instanceof WeakSet);
}
exports.assertHasPrototypeProperty = assertHasPrototypeProperty;
exports.classLooksLike = classLooksLike;
exports.classOwnKeys = classOwnKeys;
exports.getAllParentsOfClass = getAllParentsOfClass;
exports.getClassPropertyDescriptor = getClassPropertyDescriptor;
exports.getClassPropertyDescriptors = getClassPropertyDescriptors;
exports.getConstructorName = getConstructorName;
exports.getNameOrDesc = getNameOrDesc;
exports.getParentOfClass = getParentOfClass;
exports.hasAllMethods = hasAllMethods;
exports.hasMethod = hasMethod;
exports.hasPrototypeProperty = hasPrototypeProperty;
exports.isCallable = isCallable;
exports.isClassConstructor = isClassConstructor;
exports.isClassMethodReference = isClassMethodReference;
exports.isConstructor = isConstructor;
exports.isKeySafe = isKeySafe;
exports.isKeyUnsafe = isKeyUnsafe;
exports.isMethod = isMethod;
exports.isSubclass = isSubclass;
exports.isSubclassOrLooksLike = isSubclassOrLooksLike;
exports.isWeakKind = isWeakKind;
module.exports = Object.assign(exports.default, exports);
//# sourceMappingURL=reflections.cjs.map