UNPKG

@aedart/support

Version:

The Ion support package

497 lines (471 loc) 16.8 kB
import { isset, descTag } from '@aedart/support/misc'; import { includesAll } from '@aedart/support/arrays'; import { FUNCTION_PROTOTYPE } from '@aedart/contracts/support/reflections'; import { merge, isset as isset$1 } from '@aedart/support/objects'; import { DANGEROUS_PROPERTIES } from '@aedart/contracts/support/objects'; /** * @aedart/support * * BSD-3-Clause, Copyright (c) 2023-present Alin Eugen Deac <aedart@gmail.com>. */ /** * 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 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 (!isset(target)) { throw new TypeError('getParentOfClass() expects a target class as argument, undefined given'); } const parent = Reflect.getPrototypeOf(target); if (parent === 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 (!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 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] = 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 (!isset$1(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, 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 (!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 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); } export { assertHasPrototypeProperty, classLooksLike, classOwnKeys, getAllParentsOfClass, getClassPropertyDescriptor, getClassPropertyDescriptors, getConstructorName, getNameOrDesc, getParentOfClass, hasAllMethods, hasMethod, hasPrototypeProperty, isCallable, isClassConstructor, isClassMethodReference, isConstructor, isKeySafe, isKeyUnsafe, isMethod, isSubclass, isSubclassOrLooksLike, isWeakKind };