Created
December 11, 2023 19:09
-
-
Save nyteshade/fd5cbb4ef7698e187bca0192b7e2d28e to your computer and use it in GitHub Desktop.
Another go at baseline JavaScript additions such as work with Descriptors. Need to compare this with code in ne-reflection and decide which I like best.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Object.assign(Object, { | |
validKey(value) { | |
return (typeof value === 'string' || typeof value === 'symbol') | |
}, | |
isObject(value) { | |
return value && (value instanceof Object || typeof value === 'object') | |
}, | |
/** | |
* The `truthyProperties` function filters an object's properties based on a | |
* configuration and returns a new object with only the truthy values. Configurations | |
* | |
* @param object - The `object` parameter is the JavaScript object for which you | |
* want to filter the truthy properties. | |
* @param config - The `config` parameter is an optional object that allows you | |
* to customize the behavior of the `truthyProperties` function. It has the | |
* following properties: | |
* @returns The function `truthyProperties` returns an object that contains the | |
* properties from the input `object` that meet certain conditions based on the | |
* provided `config`. | |
*/ | |
truthyProperties( | |
object, | |
config | |
) { | |
const defaultConfig = { | |
allowUndefined: false, | |
allowNull: false, | |
allowOther: null /* ([key, value]) => boolean */, | |
allowAccessors: false, | |
allowFalseBooleans: true, | |
} | |
const suppliedConfig = { ...defaultConfig, ...(config ?? {}) } | |
const { | |
allowUndefined, | |
allowNull, | |
allowOther, | |
allowAccessors, | |
allowFalseBooleans | |
} = suppliedConfig | |
const [KEY, VALUE] = [0, 1] | |
const descriptors = Descriptor.all(object) | |
const entries = descriptors.entries.map(([key, descriptor]) => { | |
const entry = [key, undefined] | |
if (Descriptor.isAccessor(descriptor) && allowAccessors) { | |
entry[VALUE] = Descriptor.getAccessor(descriptor) | |
} | |
else if (Descriptor.isData(descriptor)) { | |
entry[VALUE] = Descriptor.getData(descriptor) | |
} | |
else { | |
entry[VALUE] = object[key] | |
} | |
return entry | |
}) | |
return (entries | |
.filter(entry => { | |
if ( | |
(allowUndefined && entry[VALUE] === undefined) || | |
(allowNull && entry[VALUE] === null) || | |
(allowFalseBooleans && typeof entry[VALUE] === 'boolean') || | |
(allowOther && allowOther(entry)) | |
) { | |
return true | |
} | |
return !!entry[VALUE] | |
}) | |
.reduce((accumulator, entry) => { | |
if (entry.accumulator) | |
Object.defineProperty(accumulator, entry[KEY], entry.boundDescriptor) | |
else | |
accumulator[entry[KEY]] = entry[VALUE] | |
return accumulator | |
}, {}) | |
) | |
} | |
}) | |
Object.assign(Reflect, { | |
/** | |
* The function checks if an object has all the specified keys. | |
* | |
* @param object - The `object` parameter is the object that we want to check if | |
* it has all the specified keys. | |
* @param keys - The `keys` parameter is a rest parameter, which means it can | |
* accept any number of arguments. In this case, it is expected to receive | |
* multiple keys as arguments. | |
* @returns a boolean value. | |
*/ | |
hasAll(object, ...keys) { | |
return Object.isObject(object) && (keys.flat(Infinity) | |
.map(key => Reflect.has(object, key)) | |
.every(has => has) | |
) | |
}, | |
/** | |
* The function checks if an object has at least one of the specified keys. | |
* | |
* @param object - The `object` parameter is the object that we want to check for | |
* the presence of certain keys. | |
* @param keys - The `keys` parameter is a rest parameter, which means it can | |
* accept any number of arguments. These arguments are the keys that we want to | |
* check if they exist in the `object`. | |
* @returns The function `hasSome` returns a boolean value indicating whether at | |
* least one of the keys provided as arguments exists in the given object. | |
*/ | |
hasSome(object, ...keys) { | |
return Object.isObject(object) && (keys.flat(Infinity) | |
.map(key => Reflect.has(object, key)) | |
.some(has => has) | |
) | |
}, | |
}) | |
class Descriptor { | |
/** | |
* The function `all` returns an object that provides information about the | |
* properties and methods of a given object and its prototype. | |
* | |
* @param object - The `object` parameter is the object for which you want to | |
* retrieve all properties and methods, including both instance | |
* properties/methods and prototype properties/methods. | |
* @returns an object that contains the descriptors of the provided object and | |
* its prototype. The returned object also has several methods and properties for | |
* accessing and manipulating the descriptors. | |
*/ | |
static all(object) { | |
if (!Object.isObject(object)) | |
return {} | |
const instance = Object.getOwnPropertyDescriptors(object) | |
const isClass = o => String(o).includes('class') | |
let prototype = null | |
if (object?.constructor?.prototype && isClass(object)) { | |
prototype = Object.getOwnPropertyDescriptors(object.constructor.prototype) | |
} | |
const result = { | |
[Symbol.for('instance')]: instance, | |
[Symbol.for('prototype')]: prototype, | |
has(named) { | |
return ( | |
Reflect.has(prototype, named) || | |
Reflect.has(instance, named) | |
) | |
}, | |
get(named) { | |
return (prototype[named] ?? instance[named]) | |
}, | |
object(named) { | |
return Reflect.has(prototype, named) | |
? prototype | |
: (Reflect.has(instance, named) ? instance : null) | |
}, | |
get prototypeKeys() { | |
return Object.keys(prototype ?? {}) | |
}, | |
get instanceKeys() { | |
return Object.keys(instance) | |
}, | |
get entries() { | |
return Object.entries(instance).concat(Object.entries(prototype ?? {})) | |
}, | |
} | |
return result | |
} | |
/** | |
* The function "static for" retrieves the descriptor of a property from an | |
* object or its prototype. | |
* | |
* @param object - The `object` parameter is the object that you want to get the | |
* property descriptor from. It can be any JavaScript object. | |
* @param property - The `property` parameter is a string that represents the | |
* name of the property you want to get the descriptor for. | |
* @returns the descriptor of the specified property in the given object. | |
*/ | |
static for(object, property) { | |
let descriptor = Object.getOwnPropertyDescriptor(object, property) | |
if (!descriptor && object?.constructor?.prototype) { | |
descriptor = Object.getOwnPropertyDescriptor( | |
object.constructor.prototype, | |
property | |
) | |
} | |
return descriptor | |
} | |
/** | |
* The function returns an object with enumerable and configurable properties | |
* based on the input parameters. | |
* | |
* @param [enumerable=false] - A boolean value indicating whether the property | |
* can be enumerated (listed) when iterating over the object's properties. | |
* @param [configurable=false] - The `configurable` parameter determines whether | |
* the property can be deleted or its attributes can be modified. If | |
* `configurable` is set to `true`, the property can be deleted and its | |
* attributes can be changed. If `configurable` is set to `false`, the property | |
* cannot be deleted and | |
* @returns An object with the properties `enumerable` and `configurable` is | |
* being returned. The values of these properties are determined by the arguments | |
* passed to the `base` function. | |
*/ | |
static base(enumerable = false, configurable = false) { | |
return { | |
enumerable, | |
configurable | |
} | |
} | |
/** | |
* The function "newAccessor" creates a new property descriptor object with a | |
* getter and setter function, along with optional enumerable and configurable | |
* flags. | |
* | |
* @param getter - The getter parameter is a function that will be used as the | |
* getter for the property. It will be called when the property is accessed. | |
* @param setter - The `setter` parameter is a function that will be used as the | |
* setter for the property. It will be called whenever the property is assigned a | |
* new value. | |
* @param [] - - `getter`: A function that will be used as the getter for the | |
* property. | |
* @returns an object with properties "get", "set", "enumerable", and | |
* "configurable". | |
*/ | |
static newAccessor( | |
getter, | |
setter, | |
{ enumerable, configurable } = Descriptor.base() | |
) { | |
return { | |
get: getter, | |
set: setter, | |
enumerable, | |
configurable | |
} | |
} | |
/** | |
* The function "newData" creates a new data object with customizable properties. | |
* | |
* @param value - The value parameter represents the value that will be assigned | |
* to the property. | |
* @param [writable=true] - The `writable` parameter determines whether the value | |
* of the property can be changed. If `writable` is set to `true`, the value can | |
* be changed. If `writable` is set to `false`, the value cannot be changed. | |
* @param [] - - `value`: The value to be assigned to the property. | |
* @returns an object with properties `value`, `enumerable`, `writable`, and | |
* `configurable`. | |
*/ | |
static newData( | |
value, | |
writable = true, | |
{ enumerable, configurable } = Descriptor.base() | |
) { | |
return { | |
value, | |
enumerable, | |
writable, | |
configurable | |
} | |
} | |
/** | |
* The function checks if an object is a valid object descriptor in JavaScript. | |
* | |
* @param object - The `object` parameter is the object that we want to check if | |
* it is a descriptor. | |
* @returns a boolean value. | |
*/ | |
static isDescriptor(object) { | |
const knownKeys = [ | |
...Descriptor.SHARED_KEYS, | |
...Descriptor.ACCESSOR_KEYS, | |
...Descriptor.DATA_KEYS, | |
] | |
return ( | |
Reflect.hasSome(knownKeys) && | |
( | |
Descriptor.isAccessor(object) || | |
Descriptor.isData(object) | |
) | |
) | |
} | |
/** | |
* The function checks if a given property or descriptor is a data property. | |
* | |
* @param descriptor_orProp - The `descriptor_orProp` parameter can be either a | |
* descriptor or a property name. | |
* @param object - The `object` parameter is the object that you want to check | |
* for data properties. | |
* @returns a boolean value. It returns `true` if the `descriptor` object has any | |
* keys that match the `DATA_KEYS` array, otherwise it returns `false`. | |
*/ | |
static isData(object_orProp, property) { | |
const needsDescriptor = ( | |
((typeof object_orProp === 'object') || object_orProp instanceof Object) && | |
property instanceof String | |
) | |
const descriptor = (needsDescriptor | |
? Descritor.for(object_orProp, property) | |
: object_orProp) | |
const { ACCESSOR_KEYS, DATA_KEYS } = this | |
if (Reflect.hasSome(descriptor, ACCESSOR_KEYS)) { | |
return false | |
} | |
if (Reflect.hasSome(descriptor, DATA_KEYS)) { | |
return true | |
} | |
return false | |
} | |
/** | |
* The function checks if a given property descriptor or property of an object is | |
* an accessor. | |
* | |
* @param object_orProp - The `descriptor_orProp` parameter can be either a | |
* descriptor object or a property name. | |
* @param property - The `object` parameter is the object that you want to check | |
* for accessor properties. | |
* @returns a boolean value. It returns true if the descriptor or property passed | |
* as an argument is an accessor descriptor, and false otherwise. | |
*/ | |
static isAccessor(object_orProp, property) { | |
const needsDescriptor = ( | |
(object_orProp && property) && | |
((typeof object_orProp === 'object') || object_orProp instanceof Object) && | |
(property instanceof String || (typeof property === 'symbol')) | |
) | |
const descriptor = (needsDescriptor | |
? Descritor.for(object_orProp, property) | |
: object_orProp) | |
const { ACCESSOR_KEYS, DATA_KEYS } = this | |
if (Reflect.hasSome(descriptor, DATA_KEYS)) { | |
return false | |
} | |
if (Reflect.hasSome(descriptor, ACCESSOR_KEYS)) { | |
return true | |
} | |
return false | |
} | |
/** | |
* The function `getData` retrieves the value of a property from an object if it | |
* exists and is a data property. | |
* | |
* @param object - The "object" parameter is the object from which we want to | |
* retrieve data. | |
* @param property - The `property` parameter is the name of the property that | |
* you want to retrieve the data from. | |
* @returns either the value of the specified property if it exists and is a data | |
* property, or undefined if the property does not exist or is not a data | |
* property. | |
*/ | |
static getData(object, property) { | |
if (typeof object !== 'object' || typeof property !== 'string') { | |
return null; | |
} | |
const descriptors = Descriptor.all(object) | |
if (descriptors.has(property)) { | |
const descriptor = descriptors.get(property) | |
if (Descriptor.isData(descriptor)) { | |
return descriptor.value | |
} | |
} | |
return undefined | |
} | |
/** | |
* The function `getAccessor` checks if an object has a getter/setter accessor | |
* for a given property and returns the accessor functions if found. | |
* | |
* @param object - The `object` parameter is the object from which we want to | |
* retrieve the accessor for a specific property. | |
* @param property - The `property` parameter is the name of the property for | |
* which we want to get the accessor. | |
* @returns an object that contains the getter and setter functions for the | |
* specified property of the given object. If the property is an accessor | |
* property (defined with a getter and/or setter), the returned object will also | |
* have additional properties such as "accessor" and "descriptor". If the | |
* property is not found or is not an accessor property, the function returns | |
* undefined. | |
*/ | |
static getAccessor(object, property) { | |
if (!Object.isObject(object)) | |
return null | |
const [GETTER, SETTER, OBJECT] = [0, 1, 2] | |
const results = [undefined, undefined, undefined] | |
const descriptors = this.all(object) | |
const isDescriptor = Descriptor.isDescriptor(object) | |
if (descriptors.has(property) || isDescriptor) { | |
const descriptor = isDescriptor ? object : descriptors.get(property) | |
if (Descriptor.isAccessor(descriptor)) { | |
results[OBJECT] = descriptors.object(property) | |
results[GETTER] = descriptor?.get | |
results[SETTER] = descriptor?.set | |
Object.assign(results, { | |
get() { this[GETTER].bind(this[OBJECT])() }, | |
set(value) { this[SETTER].bind(this[OBJECT])(value) }, | |
get accessor() { return true }, | |
get descriptor() { return descriptor }, | |
get boundDescriptor() { | |
return { | |
...descriptor, | |
get: descriptor.get?.bind(object), | |
set: descriptor.set?.bind(object), | |
} | |
} | |
}) | |
return results | |
} | |
} | |
return undefined | |
} | |
/** | |
* A base descriptor (new for each read) that is both enumerable and configurable | |
* | |
* @returns The method `flexible` is returning the result of calling the `base` | |
* method with the arguments `true` and `true`. | |
*/ | |
static get flexible() { | |
return this.base(true, true) | |
} | |
/** | |
* A base descriptor (new for each read) that is not enumerable but is configurable | |
* | |
* @returns The method `enigmatic` is returning the result of calling the `base` | |
* method with the arguments `false` and `true`. | |
*/ | |
static get enigmatic() { | |
return this.base(false, true) | |
} | |
/** | |
* A base descriptor (new for each read) that is neither enumerable nor configurable | |
* | |
* @returns The code is returning the result of calling the `base` method with | |
* the arguments `false` and `false`. | |
*/ | |
static get intrinsic() { | |
return this.base(false, false) | |
} | |
/** | |
* A base descriptor (new for each read) that enumerable but not configurable | |
* | |
* @returns The method is returning the result of calling the `base` method with | |
* the arguments `true` and `false`. | |
*/ | |
static get transparent() { | |
return this.base(true, false) | |
} | |
/** | |
* The function returns an array of shared descriptor keys. | |
* | |
* @returns An array containing the strings 'configurable' and 'enumerable'. | |
*/ | |
static get SHARED_KEYS() { | |
return ['configurable', 'enumerable'] | |
} | |
/** | |
* The function returns an array of accessor descriptor keys. | |
* | |
* @returns An array containing the strings 'get' and 'set' is being returned. | |
*/ | |
static get ACCESSOR_KEYS() { | |
return ['get', 'set'] | |
} | |
/** | |
* The function returns an array of data descriptor keys. | |
* | |
* @returns An array containing the strings 'value' and 'writable' is being | |
* returned. | |
*/ | |
static get DATA_KEYS() { | |
return ['value', 'writable'] | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment