diff --git a/CHANGELOG.md b/CHANGELOG.md index f8e9901e..65e35a7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +* `hasMeta()` util, in `@aedart/support/meta`. +* `MetaRepository`, `TargetMetaRepository`, `TargetContext`, `Entry`, and `hasTargetMeta()` in `@aedart/support/meta`. +* `ClassDecorator`, `ClassMethodDecorator`, `ClassGetterDecorator`, `ClassSetterDecorator`, `ClassFieldDecorator`, `ClassAutoAccessorDecorator`, and `Decorator` types, in `@aedart/contracts`. +* `ClassDecoratorResult`, `ClassMethodDecoratorResult`, `ClassGetterDecoratorResult`, `ClassSetterDecoratorResult`, `ClassFieldDecoratorResult`, `ClassAutoAccessorDecoratorResult`, and `DecoratorResult` types, in `@aedart/contracts`. + +### Changed + +**Breaking** + +* `targetMeta()` and `inheritTargetMeta()` now throw `MetaError` (_`TypeError` was previously thrown_), in `@aedart/support/meta`. + +**Non-breaking Changes** + +* Refactored / Redesigned `meta()`, `getMeta()`, and `getAllMeta()` to use new `MetaRepository` as its underlying core component for dealing with metadata. +* Refactored / Redesigned `targetMeta()`, `getTargetMeta()`, and `inheritTargetMeta()` to use new `TargetMetaRepository` underneath. +* Return type of `meta()` changed to `Decorator`. +* `MetaTargetContext` expanded with a `context: Context` property. `@aedart/contracts/support/meta`. + ### Fixed * Broken links in support/exceptions and in support/objects docs. +### Removed + +* `ClassContext`, `MethodContext`, `GetterContext`, `SetterContext`, `FieldContext`, `AccessorContext` and `MetadataContext`, in `@aedart/contracts/support/meta` (_components were deprecated in `v0.7.0`_). +* `MemberContext` type in `@aedart/contracts/support/meta` (_type was deprecated in `v0.7.0`_). + ## [0.9.0] - 2024-03-05 ### Added diff --git a/packages/contracts/src/decorators.ts b/packages/contracts/src/decorators.ts new file mode 100644 index 00000000..5fcadbde --- /dev/null +++ b/packages/contracts/src/decorators.ts @@ -0,0 +1,93 @@ +/** + * Class Decorator Result + */ +export type ClassDecoratorResult = object | void; + +/** + * Class Method Decorator Result + */ +export type ClassMethodDecoratorResult = object | void; + +/** + * Class Getter Decorator Result + */ +export type ClassGetterDecoratorResult = object | void; + +/** + * Class Setter Decorator Result + */ +export type ClassSetterDecoratorResult = object | void; + +/** + * Class Field Decorator Result + */ +export type ClassFieldDecoratorResult = (initialValue: unknown) => unknown | void; + +/** + * Class Auto-Accessor Decorator Result + */ +export type ClassAutoAccessorDecoratorResult = ClassAccessorDecoratorResult | void + +/** + * Decorator Result + */ +export type DecoratorResult = ClassDecoratorResult + | ClassMethodDecoratorResult + | ClassGetterDecoratorResult + | ClassSetterDecoratorResult + | ClassFieldDecoratorResult + | ClassAutoAccessorDecoratorResult; + +/** + * Class Decorator + * + * @see https://github.com/tc39/proposal-decorators + */ +export type ClassDecorator = (target: object, context: ClassDecoratorContext) => ClassDecoratorResult; + +/** + * Class Method Decorator + * + * @see https://github.com/tc39/proposal-decorators + */ +export type ClassMethodDecorator = (target: object, context: ClassMethodDecoratorContext) => ClassMethodDecoratorResult; + +/** + * Class Getter Decorator + * + * @see https://github.com/tc39/proposal-decorators + */ +export type ClassGetterDecorator = (target: object, context: ClassGetterDecoratorContext) => ClassGetterDecoratorResult; + +/** + * Class Setter Decorator + * + * @see https://github.com/tc39/proposal-decorators + */ +export type ClassSetterDecorator = (target: object, context: ClassSetterDecoratorContext) => ClassSetterDecoratorResult; + +/** + * Class Field Decorator + * + * @see https://github.com/tc39/proposal-decorators + */ +export type ClassFieldDecorator = (target: object, context: ClassFieldDecoratorContext) => ClassFieldDecoratorResult; + +/** + * Class Auto-Accessor Decorator + * + * @see https://github.com/tc39/proposal-decorators + */ +export type ClassAutoAccessorDecorator = (target: ClassAccessorDecoratorTarget, context: ClassAccessorDecoratorContext) => ClassAutoAccessorDecoratorResult; + +/** + * Decorator + * + * @see https://github.com/tc39/proposal-decorators + */ +export type Decorator = ClassDecorator + | ClassMethodDecorator + | ClassGetterDecorator + | ClassSetterDecorator + | ClassFieldDecorator + | ClassAutoAccessorDecorator; \ No newline at end of file diff --git a/packages/contracts/src/support/meta/AccessorContext.ts b/packages/contracts/src/support/meta/AccessorContext.ts deleted file mode 100644 index 34ff4ef0..00000000 --- a/packages/contracts/src/support/meta/AccessorContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -import MetadataContext from "./MetadataContext"; - -/** - * @deprecated Replaced by {@link ClassAccessorDecoratorContext} - * - * Class Auto-Accessor Decorator Context - */ -export default interface AccessorContext extends ClassAccessorDecoratorContext, MetadataContext -{} \ No newline at end of file diff --git a/packages/contracts/src/support/meta/ClassContext.ts b/packages/contracts/src/support/meta/ClassContext.ts deleted file mode 100644 index 19ddc7ad..00000000 --- a/packages/contracts/src/support/meta/ClassContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -import MetadataContext from "./MetadataContext"; - -/** - * @deprecated Replaced by {@link ClassDecoratorContext} - * - * Class Decorator Context - */ -export default interface ClassContext extends ClassDecoratorContext, MetadataContext -{} \ No newline at end of file diff --git a/packages/contracts/src/support/meta/FieldContext.ts b/packages/contracts/src/support/meta/FieldContext.ts deleted file mode 100644 index 8fc5caab..00000000 --- a/packages/contracts/src/support/meta/FieldContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -import MetadataContext from "./MetadataContext"; - -/** - * @deprecated Replaced by {@link ClassFieldDecoratorContext} - * - * Class Field Decorator Context - */ -export default interface FieldContext extends ClassFieldDecoratorContext, MetadataContext -{} \ No newline at end of file diff --git a/packages/contracts/src/support/meta/GetterContext.ts b/packages/contracts/src/support/meta/GetterContext.ts deleted file mode 100644 index dc213f72..00000000 --- a/packages/contracts/src/support/meta/GetterContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -import MetadataContext from "./MetadataContext"; - -/** - * @deprecated Replaced by {@link ClassGetterDecoratorContext} - * - * Class Getter Decorator Context - */ -export default interface GetterContext extends ClassGetterDecoratorContext, MetadataContext -{} \ No newline at end of file diff --git a/packages/contracts/src/support/meta/MetaEntry.ts b/packages/contracts/src/support/meta/MetaEntry.ts index 4e87a82c..a3a1e192 100644 --- a/packages/contracts/src/support/meta/MetaEntry.ts +++ b/packages/contracts/src/support/meta/MetaEntry.ts @@ -10,12 +10,12 @@ export default interface MetaEntry * * @type {Key} */ - key: Key, + key: Key; /** * Value to store * * @type {unknown} */ - value: unknown + value: unknown; } \ No newline at end of file diff --git a/packages/contracts/src/support/meta/MetaTargetContext.ts b/packages/contracts/src/support/meta/MetaTargetContext.ts index 3db816a7..b33eb7e3 100644 --- a/packages/contracts/src/support/meta/MetaTargetContext.ts +++ b/packages/contracts/src/support/meta/MetaTargetContext.ts @@ -1,3 +1,5 @@ +import { Context } from './types'; + /** * Meta Decorator Target Context */ @@ -5,16 +7,29 @@ export default interface MetaTargetContext { /** * The class that owns the meta + * + * @type {object} */ - owner: object, + owner: object; /** * "This" argument + * + * @type {any} */ - thisArg: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + thisArg: any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ /** * The target class, field, method... that is being decorated + * + * @type {object} + */ + target: object; + + /** + * Decorator context + * + * @type {Context} */ - target: object, + context: Context; } \ No newline at end of file diff --git a/packages/contracts/src/support/meta/MetadataContext.ts b/packages/contracts/src/support/meta/MetadataContext.ts deleted file mode 100644 index d6f8555a..00000000 --- a/packages/contracts/src/support/meta/MetadataContext.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { MetadataRecord } from "./types"; - -/** - * @deprecated Replaced by {@link DecoratorMetadata} - * - * Metadata Context - * - * @see https://github.com/tc39/proposal-decorator-metadata - */ -export default interface MetadataContext -{ - /** - * Contains arbitrary information - */ - metadata?: MetadataRecord; -} \ No newline at end of file diff --git a/packages/contracts/src/support/meta/MethodContext.ts b/packages/contracts/src/support/meta/MethodContext.ts deleted file mode 100644 index f33858d1..00000000 --- a/packages/contracts/src/support/meta/MethodContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -import MetadataContext from "./MetadataContext"; - -/** - * @deprecated Replaced by {@link ClassMethodDecoratorContext} - * - * Class Method Decorator Context - */ -export default interface MethodContext extends ClassMethodDecoratorContext, MetadataContext -{} \ No newline at end of file diff --git a/packages/contracts/src/support/meta/Repository.ts b/packages/contracts/src/support/meta/Repository.ts new file mode 100644 index 00000000..c5bee737 --- /dev/null +++ b/packages/contracts/src/support/meta/Repository.ts @@ -0,0 +1,67 @@ +import { DecoratorResult } from "@aedart/contracts"; +import { Key } from "@aedart/contracts/support"; +import { Context, MetaCallback, MetadataRecord } from "./types"; + +/** + * Meta Repository + */ +export default interface Repository +{ + /** + * The owner class + * + * @type {object} + */ + readonly owner: object; + + /** + * Set value for given key + * + * **Caution**: _Method is intended to be invoked inside a decorator!_ + * + * @param {object} target Decorator target, e.g. class, field, method...etc + * @param {Context} context + * @param {Key | MetaCallback} key + * @param {any} [value] Value to be stored. Ignored if `key` argument is a callback. + * + * @return {DecoratorResult} + */ + set( + target: object, + context: Context, + key: Key | MetaCallback, + value?: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): DecoratorResult; + + /** + * Get value for given key + * + * @template T Return value type + * @template D=any Type of default value + * + * @param {Key} key + * @param {D} [defaultValue] + * + * @return {T | D | undefined} + */ + get< + T, + D = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(key: Key, defaultValue?: D): T | D | undefined; + + /** + * Determine if value exists for key + * + * @param {Key} key + * + * @return {boolean} + */ + has(key: Key): boolean; + + /** + * Get all metadata + * + * @return {MetadataRecord} + */ + all(): MetadataRecord; +} \ No newline at end of file diff --git a/packages/contracts/src/support/meta/SetterContext.ts b/packages/contracts/src/support/meta/SetterContext.ts deleted file mode 100644 index 8f505505..00000000 --- a/packages/contracts/src/support/meta/SetterContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -import MetadataContext from "./MetadataContext"; - -/** - * @deprecated Replaced by {@link ClassSetterDecoratorContext} - * - * Class Setter Decorator Context - */ -export default interface SetterContext extends ClassSetterDecoratorContext, MetadataContext -{} \ No newline at end of file diff --git a/packages/contracts/src/support/meta/TargetRepository.ts b/packages/contracts/src/support/meta/TargetRepository.ts new file mode 100644 index 00000000..6bf4f05b --- /dev/null +++ b/packages/contracts/src/support/meta/TargetRepository.ts @@ -0,0 +1,75 @@ +import { Key } from "@aedart/contracts/support"; +import { Context, MetaCallback } from "./types"; +import { ClassDecoratorResult, ClassMethodDecoratorResult } from "@aedart/contracts"; + +/** + * Meta Target Repository + * + * Responsible for associating metadata directory with a target class or class method. + */ +export default interface TargetRepository +{ + /** + * Set value for given key, and associates it directly with the target + * + * **Caution**: _Method is intended to be invoked inside a decorator!_ + * + * @param {object} target Class or class method target + * @param {Context} context + * @param {Key | MetaCallback} key + * @param {any} [value] Value to be stored. Ignored if `key` argument is a callback. + * + * @return {ClassDecoratorResult | ClassMethodDecoratorResult} + * + * @throws {MetaException} + */ + set( + target: object, + context: Context, + key: Key | MetaCallback, + value?: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): ClassDecoratorResult | ClassMethodDecoratorResult; + + /** + * Get value for given key + * + * @template T Return value type + * @template D=any Type of default value + * + * @param {object} target Class or class method target + * @param {Key} key + * @param {D} [defaultValue] + * + * @return {T | D | undefined} + */ + get< + T, + D = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(target: object, key: Key, defaultValue?: D): T | D | undefined; + + /** + * Determine if value exists for key + * + * @param {object} target Class or class method target + * @param {Key} key + * + * @return {boolean} + */ + has(target: object, key: Key): boolean; + + /** + * Inherit "target" meta from a base class. + * + * **Note**: _Method is intended to be used as a decorator for static class methods, + * in situations where you overwrite static methods and wish to inherit + * "target" meta from the parent method._ + * + * @param {object} target + * @param {Context} context + * + * @return {ClassMethodDecoratorResult} + * + * @throws {MetaException} + */ + inherit(target: object, context: Context): ClassMethodDecoratorResult; +} \ No newline at end of file diff --git a/packages/contracts/src/support/meta/exceptions/MetaException.ts b/packages/contracts/src/support/meta/exceptions/MetaException.ts new file mode 100644 index 00000000..36093630 --- /dev/null +++ b/packages/contracts/src/support/meta/exceptions/MetaException.ts @@ -0,0 +1,8 @@ +import { Throwable } from "@aedart/contracts/support/exceptions"; + +/** + * Meta Exception + * + * To be thrown when metadata cannot be obtained or written for an owner class + */ +export default interface MetaException extends Throwable {} \ No newline at end of file diff --git a/packages/contracts/src/support/meta/exceptions/index.ts b/packages/contracts/src/support/meta/exceptions/index.ts new file mode 100644 index 00000000..23a6e970 --- /dev/null +++ b/packages/contracts/src/support/meta/exceptions/index.ts @@ -0,0 +1,4 @@ +import MetaException from "./MetaException"; +export { + type MetaException +} \ No newline at end of file diff --git a/packages/contracts/src/support/meta/index.ts b/packages/contracts/src/support/meta/index.ts index 91815591..55646bf3 100644 --- a/packages/contracts/src/support/meta/index.ts +++ b/packages/contracts/src/support/meta/index.ts @@ -1,12 +1,7 @@ -import MetadataContext from "./MetadataContext"; -import ClassContext from "./ClassContext"; -import MethodContext from "./MethodContext"; -import GetterContext from "./GetterContext"; -import SetterContext from "./SetterContext"; -import FieldContext from "./FieldContext"; -import AccessorContext from "./AccessorContext"; import MetaEntry from "./MetaEntry"; import MetaTargetContext from "./MetaTargetContext"; +import Repository from "./Repository"; +import TargetRepository from "./TargetRepository"; import Kind from "./Kind"; /** @@ -32,18 +27,13 @@ export const METADATA: unique symbol = Symbol.for('metadata'); export const TARGET_METADATA: unique symbol = Symbol('target_metadata'); export { - type ClassContext, - type MethodContext, - type GetterContext, - type SetterContext, - type FieldContext, - type AccessorContext, - type MetadataContext, - type MetaEntry, type MetaTargetContext, + type Repository, + type TargetRepository, Kind }; +export * from './exceptions/index'; export type * from './types'; \ No newline at end of file diff --git a/packages/contracts/src/support/meta/types.ts b/packages/contracts/src/support/meta/types.ts index 1d943e1b..ab4639a1 100644 --- a/packages/contracts/src/support/meta/types.ts +++ b/packages/contracts/src/support/meta/types.ts @@ -1,29 +1,11 @@ -import MethodContext from "./MethodContext"; -import GetterContext from "./GetterContext"; -import SetterContext from "./SetterContext"; -import FieldContext from "./FieldContext"; -import AccessorContext from "./AccessorContext"; -import MetaEntry from "./MetaEntry"; import type { Key } from "@aedart/contracts/support"; +import MetaEntry from "./MetaEntry"; /** * Decorator context types for any decorator */ export type Context = DecoratorContext; -/** - * @deprecated Replaced by {@link ClassMemberDecoratorContext} - * - * Decorator context types for class element decorators - */ -export type MemberContext = - | MethodContext - | GetterContext - | SetterContext - | FieldContext - | AccessorContext - ; - /** * Callback that returns a meta entry object. */ @@ -39,6 +21,15 @@ export type MetadataRecord = DecoratorMetadata; */ export type MetaOwnerReference = WeakRef; +/** + * Initializer callback + * + * @see ClassDecoratorContext.addInitializer + */ +export type InitializerCallback = ( + this: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +) => void; + /** * A location (key or path) to a metadata entry, in a given owner object */ diff --git a/packages/contracts/src/types.ts b/packages/contracts/src/types.ts index 1cf6b933..b19e0e14 100644 --- a/packages/contracts/src/types.ts +++ b/packages/contracts/src/types.ts @@ -1,3 +1,5 @@ +export * from './decorators'; + /** * Primitive value * diff --git a/packages/support/src/meta/Entry.ts b/packages/support/src/meta/Entry.ts new file mode 100644 index 00000000..d90b92e5 --- /dev/null +++ b/packages/support/src/meta/Entry.ts @@ -0,0 +1,101 @@ +import type { Key } from "@aedart/contracts/support"; +import type { MetaCallback, MetaEntry, MetaTargetContext } from "@aedart/contracts/support/meta"; +import { mergeKeys } from "@aedart/support/misc"; + +/** + * Meta Entry + * + * @see MetaEntry + */ +export default class Entry implements MetaEntry +{ + /** + * Key or path identifier + * + * @type {Key} + */ + key: Key; + + /** + * Value to store + * + * @type {unknown} + */ + value: unknown; + + /** + * Create a new Meta Entry instance + * + * @param {Key} key + * @param {unknown} value + */ + constructor(key: Key, value: unknown) { + this.key = key; + this.value = value; + } + + /** + * Create a new Meta Entry instance + * + * @param {Key} key + * @param {unknown} value + * + * @return {this|MetaEntry} + * + * @static + */ + public static make(key: Key, value: unknown): MetaEntry + { + return new this(key as Key, value); + } + + /** + * Resolves given key and returns a new Meta Entry instance + * + * @param {MetaTargetContext} targetContext + * @param {Key | MetaCallback} key + * @param {any} [value] + * + * @return {this|MetaEntry} + * + * @static + */ + public static resolve( + targetContext: MetaTargetContext, + key: Key | MetaCallback, + value?: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): MetaEntry + { + if (typeof key === 'function') { + return (key as MetaCallback)(targetContext.target, targetContext.context, targetContext.owner); + } + + return this.make(key as Key, value); + } + + /** + * Resolves given key-value pair and returns a new Meta Entry instance, with prefixed key + * + * @param {MetaTargetContext} targetContext + * @param {Key} prefixKey + * @param {Key|MetaCallback} key + * @param {unknown} [value] + * + * @return {this|MetaEntry} + * + * @static + */ + public static resolveWithPrefix( + targetContext: MetaTargetContext, + prefixKey: Key, + key: Key | MetaCallback, + value?: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): MetaEntry + { + const entry = this.resolve(targetContext, key, value); + + entry.key = mergeKeys(prefixKey, entry.key); + + return entry; + } +} \ No newline at end of file diff --git a/packages/support/src/meta/MetaRepository.ts b/packages/support/src/meta/MetaRepository.ts new file mode 100644 index 00000000..90cc0b85 --- /dev/null +++ b/packages/support/src/meta/MetaRepository.ts @@ -0,0 +1,362 @@ +import type { DecoratorResult } from "@aedart/contracts"; +import type { + Context, + MetaCallback, + MetadataRecord, + MetaEntry, + MetaTargetContext, + Repository, + InitializerCallback +} from "@aedart/contracts/support/meta"; +import { METADATA } from "@aedart/contracts/support/meta"; +import type { Key } from "@aedart/contracts/support"; +import { set, get, has, merge } from "@aedart/support/objects"; +import Entry from "./Entry"; +import TargetContext from "./TargetContext"; + +/** + * Fallback registry that contains writable metadata (`context.metadata`). + * + * This registry is only to be used when the system / browser does not support + * `context.metadata`. + * + * **Warning**: _This registry is **NOT intended** to be available for writing, + * outside the scope of a "meta" decorator._ + * + * @type {WeakMap} + */ +const registry: WeakMap = new WeakMap(); + +/** + * Meta Repository + * + * @see Repository + */ +export default class MetaRepository implements Repository +{ + /** + * The owner class + * + * @type {object} + * + * @private + */ + readonly #owner: object; + + /** + * Create a new Meta Repository instance + * + * @param {object} owner + */ + constructor(owner: object) { + this.#owner = owner; + } + + /** + * Create a new Meta Repository instance + * + * @param {object} owner + * + * @return {this|Repository} + */ + public static make(owner: object): Repository + { + return new this(owner); + } + + /** + * The owner class + * + * @type {object} + */ + public get owner(): object + { + return this.#owner; + } + + /** + * Set value for given key + * + * **Caution**: _Method is intended to be invoked inside a decorator!_ + * + * @param {object} target Decorator target, e.g. class, field, method...etc + * @param {Context} context + * @param {Key | MetaCallback} key + * @param {any} [value] Value to be stored. Ignored if `key` argument is a callback. + * + * @return {DecoratorResult} + */ + public set( + target: object, + context: Context, + key: Key | MetaCallback, + value?: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): DecoratorResult + { + const save = this.save.bind(this); + const resolveTargetContext = this.resolveMetaTargetContext.bind(this); + + switch(context.kind) { + // For a class target, the meta can be added directly. + case 'class': + return save( + resolveTargetContext(target, target, context), + key, + value + ); + + // When a field is decorated, we need to rely on the value initialisation to + // obtain correct owner... + case 'field': + return function(initialValue: unknown) { + save( + // @ts-expect-error: "this" corresponds to class instance. + resolveTargetContext(target, this, context), + key, + value + ); + + return initialValue; + } + + // For all other kinds of targets, we need to use the initialisation logic + // to obtain the correct owner. This is needed for current implementation + // and until the TC39 proposal is approved and implemented. + // @see https://github.com/tc39/proposal-decorator-metadata + default: + context.addInitializer(function() { + save( + // @ts-expect-error: "this" corresponds to class instance. + resolveTargetContext(target, this, context), + key, + value + ); + }); + return; + } + } + + /** + * Get value for given key + * + * @template T Return value type + * @template D=any Type of default value + * + * @param {Key} key + * @param {D} [defaultValue] + * + * @return {T | D | undefined} + */ + public get< + T, + D = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(key: Key, defaultValue?: D): T | D | undefined + { + return get(this.all(), key, defaultValue); + } + + /** + * Determine if value exists for key + * + * @param {Key} key + */ + public has(key: Key): boolean + { + return has(this.all(), key); + } + + /** + * Get all metadata + * + * @return {MetadataRecord} + */ + public all(): MetadataRecord + { + return this.owner[METADATA as keyof typeof this.owner] as MetadataRecord || {} as MetadataRecord + } + + /** + * Save metadata + * + * @param {MetaTargetContext} targetContext + * @param {Key | MetaCallback} key + * @param {any} [value] + * + * @return {void} + * + * @protected + */ + protected save( + targetContext: MetaTargetContext, + key: Key | MetaCallback, + value?: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): void + { + const context: Context = targetContext.context; + const metadata: MetadataRecord = this.resolveMetadataRecord(targetContext.owner, context); + + // Whenever the key is a "meta" callback, for any other kind than a class or a field, + // we overwrite the "context.addInitializer" method, so init callbacks can be invoked + // manually after meta has been defined. + const callbacks: InitializerCallback[] = []; + if (typeof key === 'function' && (context.kind !== 'class' && context.kind !== 'field')) { + context.addInitializer = (callback: InitializerCallback) => { + callbacks.push(callback); + } + } + + // Resolve meta entry (key and value). When a "meta callback" is given, it is invoked + // here. Afterward, set the resolved key-value. + const entry: MetaEntry = this.resolveEntry( + targetContext, + key, + value, + ); + + set(metadata, entry.key, entry.value); + + // When the metadata originates from the decorator context, we can stop here. + // Otherwise, we need to save it in the internal registry... + if (this.useMetadataFromContext(context)) { + this.runInitCallbacks(targetContext, callbacks); + return; + } + + registry.set(targetContext.owner, metadata); + + // Lastly, define the owner[Symbol.metadata] property (only done once for the owner). + // In case that owner is a subclass, then this ensures that it "overwrites" the parent's + // [Symbol.metadata] property and offers its own version thereof. + this.defineMetadataProperty(targetContext.owner); + + // Invoke evt. init callbacks... + this.runInitCallbacks(targetContext, callbacks); + } + + /** + * Defines the {@link METADATA} property in given owner + * + * @param {object} owner + * + * @return {void} + * + * @protected + */ + protected defineMetadataProperty(owner: object): void + { + Reflect.defineProperty(owner, METADATA, { + get: () => { + // To ensure that metadata cannot be changed outside the scope and context of a + // meta decorator, a deep clone of the record is returned here. + return merge( + Object.create(null), + registry.get(owner) || Object.create(null) + ); + }, + + // Ensure that the property cannot be deleted + configurable: false + }); + } + + /** + * Invokes the given initialisation callbacks + * + * @param {MetaTargetContext} targetContext + * @param {InitializerCallback[]} callbacks + * + * @return {void} + * + * @protected + */ + protected runInitCallbacks(targetContext: MetaTargetContext, callbacks: InitializerCallback[]): void + { + callbacks.forEach((callback) => { + callback.call(targetContext.thisArg); + }); + } + + /** + * Determine if metadata record can be used from decorator context + * + * @param {Context} context + * + * @return {boolean} + * + * @protected + */ + protected useMetadataFromContext(context: Context): boolean + { + return Reflect.has(context, 'metadata') && typeof context.metadata == 'object'; + } + + /** + * Resolve the metadata record that must be used when writing new metadata + * + * @param {object} owner + * @param {Context} context + * + * @protected + */ + protected resolveMetadataRecord(owner: object, context: Context): MetadataRecord + { + if (this.useMetadataFromContext(context)) { + return context.metadata as MetadataRecord; + } + + // Obtain record from registry, or create new empty object. + let metadata: MetadataRecord = registry.get(owner) ?? Object.create(null); + + // In case that the owner has Symbol.metadata defined (e.g. from base class), + // then merge it current metadata. This ensures that inheritance works as + // intended, whilst a base class still keeping its original metadata. + if (Reflect.has(owner, METADATA)) { + metadata = Object.assign(metadata, owner[METADATA as keyof typeof owner]); + } + + return metadata; + } + + /** + * Resolve the "meta" entry's key and value + * + * @param {MetaTargetContext} targetContext + * @param {Key | MetaCallback} key + * @param {any} [value] + * + * @return {MetaEntry} + * + * @protected + */ + protected resolveEntry( + targetContext: MetaTargetContext, + key: Key | MetaCallback, + value?: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): MetaEntry + { + return Entry.resolve(targetContext, key, value); + } + + /** + * Resolve the meta target context + * + * **Caution**: _`thisArg` should only be set from an "addInitializer" callback + * function, via decorator context._ + * + * @param {object} target Target the is being decorated + * @param {object} thisArg The bound "this" value, from "addInitializer" callback function. + * @param {Context} context + * + * @return {MetaTargetContext} + * + * @protected + */ + protected resolveMetaTargetContext( + target: object, + thisArg: object, + context: Context + ): MetaTargetContext + { + return TargetContext.resolveOwner(target, thisArg, context); + } +} \ No newline at end of file diff --git a/packages/support/src/meta/TargetContext.ts b/packages/support/src/meta/TargetContext.ts new file mode 100644 index 00000000..14528dca --- /dev/null +++ b/packages/support/src/meta/TargetContext.ts @@ -0,0 +1,106 @@ +import {Context, MetaTargetContext} from "@aedart/contracts/support/meta"; + +/** + * Meta Target Context + * + * @see MetaTargetContext + */ +export default class TargetContext implements MetaTargetContext +{ + /** + * The class that owns the meta + * + * @type {object} + */ + owner: object; + + /** + * "This" argument + * + * @type {any} + */ + thisArg: any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + /** + * The target class, field, method... that is being decorated + * + * @type {object} + */ + target: object; + + /** + * Decorator context + * + * @type {Context} + */ + context: Context; + + /** + * Create a new Meta Target Context instance + * + * @param {object} owner + * @param {any} thisArg + * @param {object} target + * @param {Context} context + */ + constructor( + owner: object, + thisArg: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + target: object, + context: Context + ) { + this.owner = owner; + this.thisArg = thisArg; + this.target = target; + this.context = context + } + + /** + * Returns a new Meta Target Context instance + * + * @param {object} owner + * @param {any} thisArg + * @param {object} target + * @param {Context} context + * + * @return {this|MetaTargetContext} + * + * @static + */ + public static make( + owner: object, + thisArg: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + target: object, + context: Context + ): MetaTargetContext + { + return new this(owner, thisArg, target, context); + } + + /** + * Resolves target's owner and returns a new Meta Target Instance + * + * @param {object} target + * @param {object} thisArg + * @param {Context} context + * + * @return {this|MetaTargetContext} + * + * @static + */ + public static resolveOwner( + target: object, + thisArg: object, + context: Context + ): MetaTargetContext + { + // Resolve the target's "owner" + // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#class_context + const owner: object = (context.kind === 'class' || context.static) + ? thisArg + // When target is not static, then it's obtainable via prototype + : (Reflect.getPrototypeOf(thisArg) as object).constructor; + + return this.make(owner, thisArg, target, context); + } +} \ No newline at end of file diff --git a/packages/support/src/meta/exceptions/MetaError.ts b/packages/support/src/meta/exceptions/MetaError.ts new file mode 100644 index 00000000..5796e2be --- /dev/null +++ b/packages/support/src/meta/exceptions/MetaError.ts @@ -0,0 +1,23 @@ +import type { MetaException } from "@aedart/contracts/support/meta"; +import { configureCustomError } from "@aedart/support/exceptions"; + +/** + * Meta Error + * + * @see MetaException + */ +export default class MetaError extends Error implements MetaException +{ + /** + * Create a new Meta Error instance + * + * @param {string} message + * @param {ErrorOptions} [options] + */ + constructor(message: string, options?: ErrorOptions) + { + super(message, options); + + configureCustomError(this); + } +} \ No newline at end of file diff --git a/packages/support/src/meta/exceptions/index.ts b/packages/support/src/meta/exceptions/index.ts new file mode 100644 index 00000000..c9754d26 --- /dev/null +++ b/packages/support/src/meta/exceptions/index.ts @@ -0,0 +1,4 @@ +import MetaError from "./MetaError"; +export { + MetaError +} \ No newline at end of file diff --git a/packages/support/src/meta/getAllMeta.ts b/packages/support/src/meta/getAllMeta.ts new file mode 100644 index 00000000..d11833ba --- /dev/null +++ b/packages/support/src/meta/getAllMeta.ts @@ -0,0 +1,16 @@ +import type { MetadataRecord } from "@aedart/contracts/support/meta"; +import { getMetaRepository } from "./getMetaRepository"; + +/** + * Returns all registered metadata for given target, if available + * + * @see getMeta + * + * @param {object} owner Class that owns metadata + * + * @returns {Readonly} + */ +export function getAllMeta(owner: object): Readonly +{ + return getMetaRepository(owner).all(); +} \ No newline at end of file diff --git a/packages/support/src/meta/getMeta.ts b/packages/support/src/meta/getMeta.ts new file mode 100644 index 00000000..d7025cd1 --- /dev/null +++ b/packages/support/src/meta/getMeta.ts @@ -0,0 +1,21 @@ +import type { Key } from "@aedart/contracts/support"; +import { getMetaRepository } from "./getMetaRepository"; + +/** + * Return metadata that matches key, for given target + * + * @see getAllMeta + * + * @template T + * @template D=unknown Type of default value + * + * @param {object} owner Class that owns metadata + * @param {Key} key Key or path identifier + * @param {D} [defaultValue=undefined] Default value to return, in case key does not exist + * + * @returns {T | D | undefined} + */ +export function getMeta(owner: object, key: Key, defaultValue?: D): T | D | undefined +{ + return getMetaRepository(owner).get(key, defaultValue); +} \ No newline at end of file diff --git a/packages/support/src/meta/getMetaRepository.ts b/packages/support/src/meta/getMetaRepository.ts new file mode 100644 index 00000000..1c2a41c0 --- /dev/null +++ b/packages/support/src/meta/getMetaRepository.ts @@ -0,0 +1,14 @@ +import type { Repository } from "@aedart/contracts/support/meta"; +import MetaRepository from "./MetaRepository"; + +/** + * Returns [Meta Repository]{@link Repository} for given owner + * + * @param {object} owner + * + * @return {Repository} + */ +export function getMetaRepository(owner: object): Repository +{ + return MetaRepository.make(owner); +} \ No newline at end of file diff --git a/packages/support/src/meta/hasMeta.ts b/packages/support/src/meta/hasMeta.ts new file mode 100644 index 00000000..537d3bae --- /dev/null +++ b/packages/support/src/meta/hasMeta.ts @@ -0,0 +1,15 @@ +import type { Key } from "@aedart/contracts/support"; +import { getMetaRepository } from "./getMetaRepository"; + +/** + * Determine if owner has metadata for given key + * + * @param {object} owner + * @param {Key} key + * + * @return {boolean} + */ +export function hasMeta(owner: object, key: Key): boolean +{ + return getMetaRepository(owner).has(key); +} \ No newline at end of file diff --git a/packages/support/src/meta/index.ts b/packages/support/src/meta/index.ts index 4a73eb1c..082583a1 100644 --- a/packages/support/src/meta/index.ts +++ b/packages/support/src/meta/index.ts @@ -1,2 +1,16 @@ +import Entry from "./Entry"; +import MetaRepository from "./MetaRepository"; +import TargetContext from "./TargetContext"; +export { + Entry, + MetaRepository, + TargetContext, +} + +export * from './getMetaRepository'; +export * from './hasMeta'; +export * from './getAllMeta'; +export * from './getMeta'; export * from './meta'; -export * from './targetMeta'; \ No newline at end of file +export * from './exceptions'; +export * from './target/index' \ No newline at end of file diff --git a/packages/support/src/meta/meta.ts b/packages/support/src/meta/meta.ts index 7a5a6fe9..9891f089 100644 --- a/packages/support/src/meta/meta.ts +++ b/packages/support/src/meta/meta.ts @@ -1,24 +1,10 @@ -import type {Key} from "@aedart/contracts/support"; +import type { Decorator } from "@aedart/contracts"; +import type { Key } from "@aedart/contracts/support"; import type { Context, - MetaCallback, - MetaEntry, - MetadataRecord, - MetaTargetContext + MetaCallback } from "@aedart/contracts/support/meta"; -import { METADATA } from "@aedart/contracts/support/meta"; -import { set, get } from "@aedart/support/objects"; -import { cloneDeep } from "lodash-es"; - -/** - * Registry that contains the writable metadata (`context.metadata`). - * - * **Warning**: _This registry is **NOT intended** to be available for writing, - * outside the scope of the meta decorator._ - * - * @type {WeakMap} - */ -const registry: WeakMap = new WeakMap(); +import { getMetaRepository } from "./getMetaRepository"; /** * Store value as metadata, for given key. @@ -26,7 +12,7 @@ const registry: WeakMap = new WeakMap = new WeakMap (void | ((initialValue: unknown) => unknown) | undefined)} + * @param {unknown} [value] Value to store. Ignored if `key` argument is a callback. + * + * @returns {Decorator} */ export function meta( key: Key | MetaCallback, value?: unknown -) { - return (target: object, context: Context) => { - - switch(context.kind) { - // For a class target, the meta can be added directly. - case 'class': - return save( - resolveMetaTargetContext(target, target, context), - context, - key, - value - ); - - // When a field is decorated, we need to rely on the value initialisation to - // obtain correct owner... - case 'field': - return function(initialValue: unknown) { - save( - // @ts-expect-error: "this" corresponds to class instance. - resolveMetaTargetContext(target, this, context), - context, - key, - value - ); - - return initialValue; - } - - // For all other kinds of targets, we need to use the initialisation logic - // to obtain the correct owner. This is needed for current implementation - // and until the TC39 proposal is approved and implemented. - // @see https://github.com/tc39/proposal-decorator-metadata - default: - context.addInitializer(function() { - save( - resolveMetaTargetContext(target, this, context), - context, - key, - value - ); - }); - return; - } - } -} - -/** - * Return metadata that matches key, for given target - * - * @see getAllMeta - * - * @template T - * @template D=unknown Type of default value - * - * @param {object} owner Class that owns metadata - * @param {Key} key Key or path identifier - * @param {D} [defaultValue=undefined] Default value to return, in case key does not exist - * - * @returns {T | D | undefined} - */ -export function getMeta(owner: object, key: Key, defaultValue?: D): T | D | undefined -{ - const metadata: Readonly | undefined = getAllMeta(owner); - if (metadata === undefined) { - return defaultValue; - } - - return get(metadata, key, defaultValue); -} - -/** - * Returns all registered metadata for given target, if available - * - * @see getMeta - * - * @param {object} owner Class that owns metadata - * - * @returns {Readonly | undefined} - */ -export function getAllMeta(owner: object): Readonly | undefined -{ - // @ts-expect-error: Owner can have Symbol.metadata defined - or not - return owner[METADATA] ?? undefined; -} - -/** - * Save metadata - * - * @param {MetaTargetContext} targetContext - * @param {Context} context Decorator context - * @param {Key | MetaCallback} key Key or path identifier. If callback is given, - * then its resulting {@link MetaEntry}'s `key` - * and `value` are stored. - * @param {unknown} [value] Value to store. Ignored if `key` argument is - * a callback. - * - * @return {void} - */ -function save( - targetContext: MetaTargetContext, - context: Context, - key: Key | MetaCallback, - value?: unknown, -) +): Decorator { - // Determine if metadata from context can be used (if it's available), and resolve it either from - // the decorator context or from the registry. - const useMetaFromContext: boolean = Reflect.has(context, 'metadata') && typeof context.metadata === 'object'; - const metadata: MetadataRecord = resolveMetadataRecord(targetContext.owner, context, useMetaFromContext); - - // Set context.metadata, in case that it didn't exist in the decorator context, when - // reaching this point. This also allows "meta callback" to access previous defined - // metadata. - // ------------- NOTE: THIS SHOULD NOT BE NEEDED. -------------------------------- - // const descriptor = Object.getOwnPropertyDescriptor(context, 'metadata'); - // if (descriptor?.writable) { - // context.metadata = metadata; - // } else { - // console.warn('context.metadata is not writable for ', targetContext); - // } - - // Whenever the key is a "meta" callback, for any other kind than a class or a field, - // we overwrite the "context.addInitializer" method, so init callbacks can be invoked - // manually after meta has been defined. - const initCallbacks: ((this: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) => void)[] = []; - if (typeof key === 'function' && (context.kind !== 'class' && context.kind !== 'field')) { - context.addInitializer = (callback: (this: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) => void) => { - initCallbacks.push(callback); - } - } - - // Resolve meta entry (key and value). When a "meta callback" is given, it is invoked - // here. Afterward, set the resolved key-value. - const entry: MetaEntry = resolveEntry( - targetContext, - context, - key, - value, - ); - - set(metadata, entry.key, entry.value); - - // When the metadata originates from the decorator context, we can stop here. - // Otherwise, we need to save it in the internal registry... - if (useMetaFromContext) { - runInitCallbacks(targetContext, initCallbacks); - return; - } - - registry.set(targetContext.owner, metadata); - - // Lastly, define the owner[Symbol.metadata] property (only done once for the owner). - // In case that owner is a subclass, then this ensures that it "overwrites" the parent's - // [Symbol.metadata] property and offers its own version thereof. - Reflect.defineProperty(targetContext.owner, METADATA, { - get: () => { - // To ensure that metadata cannot be changed outside the scope and context of a - // meta decorator, a deep clone of the record is returned here. JavaScript's - // native structuredClone cannot be used, because it does not support symbols. - return cloneDeep(registry.get(targetContext.owner)); - }, - - // Ensure that the property cannot be deleted - configurable: false - }); - - // Invoke evt. init callbacks... - runInitCallbacks(targetContext, initCallbacks); -} - -/** - * Resolve the metadata record that must be used when writing new metadata - * - * @param {object} owner - * @param {Context} context - * @param {boolean} useMetaFromContext - * - * @returns {MetadataRecord} - */ -function resolveMetadataRecord(owner: object, context: Context, useMetaFromContext: boolean): MetadataRecord -{ - // If registry is not to be used, it means that context.metadata is available - if (useMetaFromContext) { - return context.metadata as MetadataRecord; - } - - // Obtain record from registry, or create new empty object. - let metadata: MetadataRecord = registry.get(owner) ?? Object.create(null); - - // In case that the owner has Symbol.metadata defined (e.g. from base class), - // then merge it current metadata. This ensures that inheritance works as - // intended, whilst a base class still keeping its original metadata. - if (Reflect.has(owner, METADATA)) { - // @ts-expect-error: Owner has Symbol.metadata! - metadata = Object.assign(metadata, owner[METADATA]); - } - - return metadata; -} - -/** - * Resolve the "meta" entry's key and value - * - * @param {MetaTargetContext} targetContext - * @param {Context} context - * @param {Key | MetaCallback} key If callback is given, then it is invoked. - * It's resulting meta entry is returned. - * @param {unknown} value Value to store as metadata. Ignored if callback is given - * as key. - * - * @returns {MetaEntry} - */ -function resolveEntry( - targetContext: MetaTargetContext, - context: Context, - key: Key | MetaCallback, - value: unknown, -): MetaEntry -{ - if (typeof key === 'function') { - return (key as MetaCallback)(targetContext.target, context, targetContext.owner); - } - - return { - key: (key as Key), - value: value - } -} - -/** - * Invokes the given initialisation callbacks - * - * @param {MetaTargetContext} targetContext - * @param {((this:any) => void)[]} callbacks - */ -function runInitCallbacks(targetContext: MetaTargetContext, callbacks: ((this: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) => void)[]): void -{ - callbacks.forEach((callback: (this: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) => void) => { - callback.call(targetContext.thisArg); - }); -} - -/** - * Resolve the meta target context - * - * **Caution**: _`thisArg` should only be set from an "addInitializer" callback - * function, via decorator context._ - * - * @param {object} target Target the is being decorated - * @param {object} thisArg The bound "this" value, from "addInitializer" callback function. - * @param {Context} context - * - * @returns {MetaTargetContext} - */ -function resolveMetaTargetContext( - target: object, - thisArg: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ - context: Context -): MetaTargetContext -{ - return { - owner: resolveTargetOwner(thisArg, context), - thisArg: thisArg, - target: target + return (target: object, context: Context) => { + return getMetaRepository({}).set(target, context, key, value); } -} - -/** - * Resolve the target's "owner" - * - * **Caution**: _`thisArg` should only be set from an "addInitializer" callback - * function, via decorator context._ - * - * @param {object} thisArg The bound "this" value, from "addInitializer" callback function. - * @param {Context} context - * - * @returns {object} Target owner class - */ -function resolveTargetOwner(thisArg: object, context: Context): object -{ - // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#class_context - return (context.kind === 'class' || context.static) - ? thisArg - // @ts-expect-error: When target is not static, then it's obtainable via prototype - : Reflect.getPrototypeOf(thisArg)?.constructor; } \ No newline at end of file diff --git a/packages/support/src/meta/target/TargetMetaRepository.ts b/packages/support/src/meta/target/TargetMetaRepository.ts new file mode 100644 index 00000000..13d22e2d --- /dev/null +++ b/packages/support/src/meta/target/TargetMetaRepository.ts @@ -0,0 +1,430 @@ +import type { ClassDecoratorResult, ClassMethodDecoratorResult } from "@aedart/contracts"; +import type { Key } from "@aedart/contracts/support"; +import { + Context, + MetaCallback, + TargetRepository, + MetaAddress, + Repository, + MetaEntry, MetaOwnerReference, type MetaTargetContext +} from "@aedart/contracts/support/meta"; +import { FUNCTION_PROTOTYPE } from "@aedart/contracts/support/reflections"; +import { METADATA, TARGET_METADATA, Kind } from "@aedart/contracts/support/meta"; +import { empty, mergeKeys, toWeakRef } from "@aedart/support/misc"; +import { ELEMENT_KIND_IDENTIFIERS, STATIC_IDENTIFIER } from "./helpers"; +import { getMetaRepository } from "../getMetaRepository"; +import Entry from "../Entry"; +import TargetContext from "../TargetContext"; +import MetaError from "../exceptions/MetaError"; + +/** + * Registry that contains the target object (e.g. a class or a method), + * along with a "meta address" that points to where the actual metadata + * is located. + * + * @see {MetaAddress} + * + * @type {WeakMap} + */ +const addressRegistry: WeakMap = new WeakMap(); + +/** + * Target Meta Repository + * + * @see TargetRepository + */ +export default class TargetMetaRepository implements TargetRepository +{ + /** + * Returns a new Target Meta Repository + * + * @return {this|TargetRepository} + */ + public static make(): TargetRepository + { + return new this(); + } + + /** + * Set value for given key, and associates it directly with the target + * + * **Caution**: _Method is intended to be invoked inside a decorator!_ + * + * @param {object} target Class or class method target + * @param {Context} context + * @param {Key | MetaCallback} key + * @param {any} [value] Value to be stored. Ignored if `key` argument is a callback. + * + * @return {ClassDecoratorResult | ClassMethodDecoratorResult} + * + * @throws {MetaError} + */ + public set( + target: object, + context: Context, + key: Key | MetaCallback, + value?: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): ClassDecoratorResult | ClassMethodDecoratorResult + { + return this.makeRepository(target) + .set( + target, + context, + this.makeMetaCallback(key, value) + ); + } + + /** + * Get value for given key + * + * @template T Return value type + * @template D=any Type of default value + * + * @param {object} target Class or class method target + * @param {Key} key + * @param {D} [defaultValue] + * + * @return {T | D | undefined} + */ + public get< + T, + D = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(target: object, key: Key, defaultValue?: D): T | D | undefined + { + // Find "target" meta address for given target object + // or return the default value if none is found. + const address: MetaAddress | undefined = this.find(target); + if (address === undefined) { + return defaultValue; + } + + // When an address was found, we must ensure that the meta + // owner class still exists. If not, return default value. + const owner: object | undefined = address[0]?.deref(); + if (owner === undefined) { + return defaultValue; + } + + // Finally, use getMeta to obtain desired key. + const prefixKey: Key = address[1]; + + return this.makeRepository(owner).get( + mergeKeys(prefixKey, key), + defaultValue + ) + } + + /** + * Determine if value exists for key + * + * @param {object} target Class or class method target + * @param {Key} key + * + * @return {boolean} + */ + public has(target: object, key: Key): boolean + { + const address: MetaAddress | undefined = this.find(target); + if (address === undefined) { + return false; + } + + const owner: object | undefined = address[0]?.deref(); + if (owner === undefined) { + return false; + } + + return this.makeRepository(owner).has( + mergeKeys(address[1], key), + ); + } + + /** + * Inherit "target" meta from a base class. + * + * **Note**: _Method is intended to be used as a decorator for static class methods, + * in situations where you overwrite static methods and wish to inherit + * "target" meta from the parent method._ + * + * @param {object} target + * @param {Context} context + * + * @return {ClassMethodDecoratorResult} + * + * @throws {MetaError} + */ + inherit(target: object, context: Context): ClassMethodDecoratorResult + { + const makePrefixKey = this.makePrefixKey.bind(this); + const makeRepository = this.makeRepository.bind(this); + + return this.set(target, context, (target: object, context: Context, owner: object) => { + const name = context.name?.toString() || 'unknown'; + + // Obtain owner's parent or fail if no parent is available. + if (Reflect.getPrototypeOf(owner) === null) { + throw new MetaError(`Unable to inherit target meta for ${name}: Owner object does not have a parent class.`, { cause: { target: target, context: context } }); + } + + // Obtain "target" meta from parent, so we can obtain a meta entry and re-set it, + // which will cause the @targetMeta() and @meta() decorators to do the rest. + const parent: object = Reflect.getPrototypeOf(owner) as object; + const prefixKey: Key = makePrefixKey(context); + const targetMeta: object | undefined = makeRepository(parent) + .get(prefixKey); + + // Abort in case that there is nothing to inherit... + if (empty(targetMeta)) { + throw new MetaError(`Unable to inherit target meta for ${name}: parent ${context.kind} does not have target meta.`, { cause: { target: target, context: context } }); + } + + // Get the first key-value pair (meta entry), from the "target" metadata + const key: Key = Reflect.ownKeys(targetMeta as object)[0]; + const value: unknown = (targetMeta as object)[key as keyof typeof targetMeta]; + + // Finally, (re)set the meta-entry. This is needed so that we do not add a "null" entry, + // other kind of useless metadata. All other meta entries are automatically handled by + // the @meta() decorator. + return Entry.make(key, value); + }); + } + + /** + * Find the address where "target" meta is stored for the given target + * + * @param {object} target + * + * @return {MetaAddress|undefined} + */ + public find(target: object): MetaAddress | undefined + { + // Return target meta address, if available for target... + let address: MetaAddress | undefined = addressRegistry.get(target); + if (address !== undefined) { + return address; + } + + // When no address is found for the target, and when a class instance is given, the actual + // target must be changed to the constructor + if (typeof target == 'object' && Reflect.has(target, 'constructor')) { + if (addressRegistry.has(target.constructor)) { + return addressRegistry.get(target.constructor); + } + + // Otherwise, change the target to the constructor. + target = target.constructor; + } + + // When no address is found and the target is a class with metadata, + // then attempt to find address via its parent. + let parent:object|null = target; + while(address === undefined && METADATA in parent) { + parent = Reflect.getPrototypeOf(parent); + if (parent === null || parent === FUNCTION_PROTOTYPE) { + break; + } + + // Attempt to get meta address from parent. + address = addressRegistry.get(parent); + } + + // Recursive version... + // if (address === undefined && METADATA in target) { + // const parent: object | null = Reflect.getPrototypeOf(target); + // + // if (parent !== null && parent !== Reflect.getPrototypeOf(Function)) { + // return this.find(parent); + // } + // } + + return address; + } + + /** + * Returns a new meta callback for given key-value pair. + * + * **Note**: _Callback is responsible for associating key-value pair with class + * or class method._ + * + * @param {Key | MetaCallback} key + * @param {any} [value] + * + * @protected + */ + protected makeMetaCallback( + key: Key | MetaCallback, + value?: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): MetaCallback + { + const makePrefixKey = this.makePrefixKey.bind(this); + const makeMetaTargetContext = this.makeMetaTargetContext.bind(this); + const makeMetaEntry = this.makeMetaEntry.bind(this); + const makeMetaAddress = this.makeMetaAddress.bind(this); + const save = this.save.bind(this); + + return (target: object, context: Context, owner: object) => { + // Prevent unsupported kinds from being decorated... + if (!['class', 'method'].includes(context.kind)) { + throw new MetaError(`@targetMeta() does not support "${context.kind}" (only "class" and "method" are supported)`, { cause: { target: target, context: context } }); + } + + // Make a "prefix" key, to be used in the final meta entry, + // and a meta address entry. + const prefixKey: Key = makePrefixKey(context); + const address: MetaAddress = makeMetaAddress(owner, prefixKey); + + // Save the address in the registry... + save(target, address); + + // When a method in a base class is decorated, but the method is overwritten in + // a subclass, then we must store another address entry, using the owner's + // method in the registry. This will allow inheriting the meta, but will NOT work + // on static methods. + if (context.kind == 'method' && !context.static && Reflect.has(owner, 'prototype')) { + // @ts-expect-error: TS2339 Owner has a prototype at this point, but Reflect.getPrototypeOf() returns undefined here! + const proto: object | undefined = (owner).prototype; + + if (proto !== undefined + && typeof proto[context.name as keyof typeof proto] == 'function' + && proto[context.name as keyof typeof proto] !== target + ) { + save(proto[context.name as keyof typeof proto], address); + } + } + + // Finally, return the meta key-value pair that will be stored in the owner's metadata. + return makeMetaEntry( + makeMetaTargetContext(owner, null, target, context), + prefixKey, + key, + value + ); + } + } + + /** + * Save metadata address in internal registry, for given target + * + * @param {object} target The target metadata is to be associated with + * @param {MetaAddress} address Location where actual metadata is to be found + * + * @return {void} + * + * @protected + */ + protected save(target: object, address: MetaAddress): void + { + addressRegistry.set(target, address); + } + + /** + * Returns a "prefix" key (path) where "target" metadata must be stored + * + * @param {Context} context + * + * @return {Key} + * + * @throws {MetaError} If {@link Context.kind} is not supported + * + * @protected + */ + protected makePrefixKey(context: Context): Key + { + if (!Reflect.has(Kind, context.kind)) { + throw new MetaError(`context.kind: "${context.kind}" is unsupported`, { cause: { context: context } }); + } + + const output: PropertyKey[] = [ + TARGET_METADATA, + ELEMENT_KIND_IDENTIFIERS[Kind[context.kind]] + ]; + + // Ensures that we do not overwrite static / none-static elements with same name! + if (context.kind !== 'class' && context.static) { + output.push(STATIC_IDENTIFIER); + } + + // "anonymous" is for anonymous classes (they do not have a name) + const name: string | symbol = context.name ?? 'anonymous'; + output.push(name); + + return output as Key; + } + + /** + * Returns a new Meta Target Context + * + * @param {object} owner + * @param {any} thisArg + * @param {object} target + * @param {Context} context + * + * @return {MetaTargetContext} + * + * @protected + */ + protected makeMetaTargetContext( + owner: object, + thisArg: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + target: object, + context: Context + ): MetaTargetContext + { + return TargetContext.make(owner, thisArg, target, context); + } + + /*** + * Returns a new metadata entry, with prefixed key + * + * @param {MetaTargetContext} targetContext + * @param {Key} prefixKey + * @param {Key|MetaCallback} key User provided key or callback + * @param {unknown} [value] Value to store. Ignored if `key` argument is + * a callback. + * + * @return {MetaEntry} + * + * @protected + */ + protected makeMetaEntry( + targetContext: MetaTargetContext, + prefixKey: Key, + key: Key | MetaCallback, + value?: unknown + ): MetaEntry + { + return Entry.resolveWithPrefix(targetContext, prefixKey, key, value); + } + + /** + * Returns a new meta address + * + * @param {object|MetaOwnerReference} owner + * @param {Key} key + * + * @return {MetaAddress} + * + * @protected + */ + protected makeMetaAddress(owner: object | MetaOwnerReference, key: Key): MetaAddress + { + return [ + toWeakRef(owner) as MetaOwnerReference, + key + ] + } + + /** + * Returns a new Repository instance for given owner + * + * @param {object} owner + * + * @return {Repository} + * + * @protected + */ + protected makeRepository(owner: object): Repository + { + return getMetaRepository(owner); + } +} \ No newline at end of file diff --git a/packages/support/src/meta/target/getTargetMeta.ts b/packages/support/src/meta/target/getTargetMeta.ts new file mode 100644 index 00000000..d4cb7579 --- /dev/null +++ b/packages/support/src/meta/target/getTargetMeta.ts @@ -0,0 +1,26 @@ +import type { Key } from "@aedart/contracts/support"; +import { getTargetMetaRepository } from "./getTargetMetaRepository"; + +/** + * Return metadata that matches key, that belongs to the given target + * + * **Note**: _Unlike the {@link getMeta} method, this method does not require you + * to know the owner object (e.g. the class) that holds metadata, provided + * that metadata has been associated with given target, via {@link targetMeta}._ + * + * @see targetMeta + * @see getMeta + * + * @template T + * @template D=unknown Type of default value + * + * @param {object} target Class or method that owns metadata + * @param {Key} key Key or path identifier + * @param {D} [defaultValue=undefined] Default value to return, in case key does not exist + * + * @returns {T | D | undefined} + */ +export function getTargetMeta(target: object, key: Key, defaultValue?: D): T | D | undefined +{ + return getTargetMetaRepository().get(target, key, defaultValue); +} diff --git a/packages/support/src/meta/target/getTargetMetaRepository.ts b/packages/support/src/meta/target/getTargetMetaRepository.ts new file mode 100644 index 00000000..998456ea --- /dev/null +++ b/packages/support/src/meta/target/getTargetMetaRepository.ts @@ -0,0 +1,12 @@ +import type { TargetRepository } from "@aedart/contracts/support/meta"; +import TargetMetaRepository from "./TargetMetaRepository"; + +/** + * Returns a new Target Meta Repository + * + * @return {TargetRepository} + */ +export function getTargetMetaRepository(): TargetRepository +{ + return TargetMetaRepository.make(); +} \ No newline at end of file diff --git a/packages/support/src/meta/target/hasTargetMeta.ts b/packages/support/src/meta/target/hasTargetMeta.ts new file mode 100644 index 00000000..3f513563 --- /dev/null +++ b/packages/support/src/meta/target/hasTargetMeta.ts @@ -0,0 +1,15 @@ +import type { Key } from "@aedart/contracts/support"; +import { getTargetMetaRepository } from "./getTargetMetaRepository"; + +/** + * Determine if value exists for key, in given target + * + * @param {object} target + * @param {Key} key + * + * @return {boolean} + */ +export function hasTargetMeta(target: object, key: Key): boolean +{ + return getTargetMetaRepository().has(target, key); +} \ No newline at end of file diff --git a/packages/support/src/meta/target/helpers.ts b/packages/support/src/meta/target/helpers.ts new file mode 100644 index 00000000..f3808ca3 --- /dev/null +++ b/packages/support/src/meta/target/helpers.ts @@ -0,0 +1,22 @@ +import { Kind } from "@aedart/contracts/support/meta"; + +/** + * Element Kind Identifiers + * + * @type {Record} + */ +export const ELEMENT_KIND_IDENTIFIERS: Record = { + [Kind.class]: Symbol('class'), + [Kind.method]: Symbol('methods'), + [Kind.getter]: Symbol('getters'), + [Kind.setter]: Symbol('setters'), + [Kind.field]: Symbol('fields'), + [Kind.accessor]: Symbol('accessors'), +}; + +/** + * Static element identifier + * + * @type {symbol} + */ +export const STATIC_IDENTIFIER: unique symbol = Symbol('static'); diff --git a/packages/support/src/meta/target/index.ts b/packages/support/src/meta/target/index.ts new file mode 100644 index 00000000..b381a22e --- /dev/null +++ b/packages/support/src/meta/target/index.ts @@ -0,0 +1,12 @@ + +import TargetMetaRepository from "./TargetMetaRepository"; +export { + TargetMetaRepository +} + +export * from './getTargetMetaRepository'; +export * from './getTargetMeta'; +export * from './hasTargetMeta'; +export * from './inheritTargetMeta'; +export * from './targetMeta'; +export * from './helpers'; \ No newline at end of file diff --git a/packages/support/src/meta/target/inheritTargetMeta.ts b/packages/support/src/meta/target/inheritTargetMeta.ts new file mode 100644 index 00000000..8c62b6b3 --- /dev/null +++ b/packages/support/src/meta/target/inheritTargetMeta.ts @@ -0,0 +1,43 @@ +import type { ClassMethodDecorator } from "@aedart/contracts"; +import type { Context } from "@aedart/contracts/support/meta"; +import { getTargetMetaRepository } from "./getTargetMetaRepository"; + +/** + * Inherit "target" meta from a base class. + * + * **Note**: _Method is intended to be used as a static method decorator!_ + * + * **Note**: _To be used in situations where you overwrite static methods and wish to inherit + * "target" meta from the parent method._ + * + * @see targetMeta + * + * @example + * ```ts + * class A { + * @targetMeta('bar', 'zaz') + * static foo() {} + * } + * + * class B extends A { + * + * @inheritTargetMeta() + * static foo() { + * // ...overwritten static method...// + * } + * } + * + * getTargetMeta(B.foo, 'bar'); // 'zaz' + * ``` + * + * @returns {ClassMethodDecorator} + * + * @throws {MetaError} When decorated element's owner class has no parent, or when no "target" metadata available + * on parent element. + */ +export function inheritTargetMeta(): ClassMethodDecorator +{ + return (target: object, context: Context) => { + return getTargetMetaRepository().inherit(target, context); + } +} diff --git a/packages/support/src/meta/target/targetMeta.ts b/packages/support/src/meta/target/targetMeta.ts new file mode 100644 index 00000000..8faa3571 --- /dev/null +++ b/packages/support/src/meta/target/targetMeta.ts @@ -0,0 +1,47 @@ +import type { + ClassDecorator, + ClassMethodDecorator +} from "@aedart/contracts"; +import type { Key } from "@aedart/contracts/support"; +import type { + Context, + MetaCallback, +} from "@aedart/contracts/support/meta"; +import { getTargetMetaRepository } from "./getTargetMetaRepository"; + +/** + * Stores value for given key, and associates it directly with the target + * + * **Note**: _Method is intended to be used as a class or method decorator!_ + * + * @example + * ```ts + * class A { + * @targetMeta('my-key', 'my-value') + * foo() {} + * } + * + * const a: A = new A(); + * getTargetMeta(a.foo, 'my-key'); // 'my-value' + * ``` + * + * @see getTargetMeta + * + * @param {Key | MetaCallback} key Key or path identifier. If callback is given, + * then its resulting [MetaEntry]{@link import('@aedart/contracts/support/meta').MetaEntry}'s `key` + * and `value` are stored. + * @param {unknown} [value] Value to store. Ignored if `key` argument is + * a callback. + * @returns {ClassDecorator | ClassMethodDecorator} + * + * @throws {MetaError} When decorated element is not supported + */ +export function targetMeta( + key: Key | MetaCallback, + value?: unknown +): ClassDecorator | ClassMethodDecorator +{ + return (target: object, context: Context) => { + return getTargetMetaRepository().set(target, context, key, value); + } +} diff --git a/packages/support/src/meta/targetMeta.ts b/packages/support/src/meta/targetMeta.ts deleted file mode 100644 index a89390a5..00000000 --- a/packages/support/src/meta/targetMeta.ts +++ /dev/null @@ -1,356 +0,0 @@ -import type { Key } from "@aedart/contracts/support"; -import type { - Context, - MetaCallback, - MetaEntry, - MetaAddress, -} from "@aedart/contracts/support/meta"; -import { FUNCTION_PROTOTYPE } from "@aedart/contracts/support/reflections"; -import { METADATA, TARGET_METADATA, Kind } from "@aedart/contracts/support/meta"; -import { mergeKeys, empty } from "@aedart/support/misc"; -import { meta, getMeta } from './meta' - -/** - * Registry that contains the target object (e.g. a class or a method), - * along with a "meta address" to where the actual metadata is located. - * - * @see {MetaAddress} - */ -const addressesRegistry: WeakMap = new WeakMap(); - -/** - * Map of identifiers to use for meta address, depending on the element kind - */ -const ELEMENT_KIND_IDENTIFIERS = { - [Kind.class]: Symbol('class'), - [Kind.method]: Symbol('methods'), - [Kind.getter]: Symbol('getters'), - [Kind.setter]: Symbol('setters'), - [Kind.field]: Symbol('fields'), - [Kind.accessor]: Symbol('accessors'), -} as const; - -/** - * Static element identifier - */ -const STATIC_IDENTIFIER: unique symbol = Symbol('static'); - -/** - * Stores value for given key, and associates it directly with the target - * - * **Note**: _Method is intended to be used as a class or method decorator!_ - * - * @example - * ```ts - * class A { - * @targetMeta('my-key', 'my-value') - * foo() {} - * } - * - * const a: A = new A(); - * getTargetMeta(a.foo, 'my-key'); // 'my-value' - * ``` - * - * @see getTargetMeta - * @see meta - * - * @param {Key | MetaCallback} key Key or path identifier. If callback is given, - * then its resulting {@link MetaEntry}'s `key` - * and `value` are stored. - * @param {unknown} [value] Value to store. Ignored if `key` argument is - * a callback. - * @returns {(target: object, context: Context) => (void | ((initialValue: unknown) => unknown) | undefined)} - * - * @throws {TypeError} When decorated element is not supported - */ -export function targetMeta( - key: Key | MetaCallback, - value?: unknown -) { - return meta((target: object, context: Context, owner: object) => { - - // Prevent unsupported kinds from being decorated... - if (!['class', 'method'].includes(context.kind)) { - throw new TypeError(`@targetMeta() does not support "${context.kind}" (only "class" and "method" are supported)`); - } - - // Make a "prefix" key, to be used in the final meta entry, - // and a meta address entry. - const prefixKey: Key = makePrefixKey(context); - const address: MetaAddress = [ - new WeakRef(owner), - prefixKey - ]; - - // Save the address in the registry... - saveAddress(target, address); - - // When a method in a base class is decorated, but the method is overwritten in - // a subclass, then we must store another address entry, using the owner's - // method in the registry. This will allow inheriting the meta, but will NOT work - // on static methods. - if (context.kind == 'method' && !context.static && Reflect.has(owner, 'prototype')) { - // @ts-expect-error: TS2339 Owner has a prototype at this point, but Reflect.getPrototypeOf() returns undefined here! - const proto: object | undefined = owner.prototype; - - if (proto !== undefined && typeof proto[context.name] == 'function' && proto[context.name] !== target) { - saveAddress(proto[context.name], address); - } - } - - // Finally, return the meta key-value pair that will be stored in the owner's metadata. - return makeMetaEntry( - target, - context, - owner, - prefixKey, - key, - value - ); - }); -} - -/** - * Inherit "target" meta from a base class. - * - * **Note**: _Method is intended to be used as a static method decorator!_ - * - * **Note**: _To be used in situations where you overwrite static methods and wish to inherit - * "target" meta from the parent method._ - * - * @see targetMeta - * - * @example - * ```ts - * class A { - * @targetMeta('bar', 'zaz') - * static foo() {} - * } - * - * class B extends A { - * - * @inheritTargetMeta() - * static foo() { - * // ...overwritten static method...// - * } - * } - * - * getTargetMeta(B.foo, 'bar'); // 'zaz' - * ``` - * - * @returns {(target: object, context: Context) => (void | ((initialValue: unknown) => unknown) | undefined)} - * - * @throws {TypeError} When decorated element's owner class has no parent, or when no "target" metadata available - * on parent element. - */ -export function inheritTargetMeta() -{ - return targetMeta((target: object, context: Context, owner: object) => { - // Obtain owner's parent or fail if no parent is available. - if (Reflect.getPrototypeOf(owner) === null) { - throw new TypeError(`Unable to inherit target meta for ${context.name}: Owner object does not have a parent class.`); - } - - // Obtain "target" meta from parent, so we can obtain a meta entry and re-set it, - // which will cause the @targetMeta() and @meta() decorators to do the rest. - const prefixKey: Key = makePrefixKey(context); - const targetMeta: object | undefined = getMeta(Reflect.getPrototypeOf(owner), prefixKey); - - // Abort in case that there is nothing to inherit... - if (empty(targetMeta)) { - throw new TypeError(`Unable to inherit target meta for ${context.name}: parent ${context.kind} does not have target meta.`); - } - - // Get the first key-value pair (meta entry), from the "target" metadata - const key: Key = Reflect.ownKeys(targetMeta)[0]; - const value: unknown = (targetMeta as object)[key]; - - // Finally, (re)set the meta-entry. This is needed so that we do not add a "null" entry, - // other kind of useless metadata. All other meta entries are automatically handled by - // the @meta() decorator. - return { - key, - value - } as MetaEntry; - }); -} - -/** - * Return metadata that matches key, that belongs to the given target - * - * **Note**: _Unlike the {@link getMeta} method, this method does not require you - * to know the owner object (e.g. the class) that holds metadata, provided - * that metadata has been associated with given target, via {@link targetMeta}._ - * - * @see targetMeta - * @see getMeta - * - * @template T - * @template D=unknown Type of default value - * - * @param {object} target Class or method that owns metadata - * @param {Key} key Key or path identifier - * @param {D} [defaultValue=undefined] Default value to return, in case key does not exist - * - * @returns {T | D | undefined} - */ -export function getTargetMeta(target: object, key: Key, defaultValue?: D): T | D | undefined -{ - // Find "target" meta address for given target object - // or return the default value if none is found. - const address: MetaAddress | undefined = findAddress(target); - if (address === undefined) { - return defaultValue; - } - - // When an address was found, we must ensure that the meta - // owner class still exists. If not, return default value. - const owner: object | undefined = address[0]?.deref(); - if (owner === undefined) { - return defaultValue; - } - - // Finally, use getMeta to obtain desired key. - const prefixKey: Key = address[1]; - return getMeta( - owner, - mergeKeys(prefixKey, key), - defaultValue - ); -} - -/** - * Find the address where "target" meta is stored for the given target - * - * @param {object} target - * - * @return {MetaAddress|undefined} - */ -function findAddress(target: object): MetaAddress | undefined -{ - // Return target meta address, if available for target... - let address: MetaAddress | undefined = addressesRegistry.get(target); - if (address !== undefined) { - return address; - } - - // When no address is found for the target, and when a class instance is given, the actual - // target must be changed to the constructor - if (typeof target == 'object' && Reflect.has(target, 'constructor')) { - if (addressesRegistry.has(target.constructor)) { - return addressesRegistry.get(target.constructor); - } - - // Otherwise, change the target to the constructor. - target = target.constructor; - } - - // When no address is found and the target is a class with metadata, - // then attempt to find address via its parent. - let parent:object|null = target; - while(address === undefined && METADATA in parent) { - parent = Reflect.getPrototypeOf(parent); - if (parent === null || parent === FUNCTION_PROTOTYPE) { - break; - } - - // Attempt to get meta address from parent. - address = addressesRegistry.get(parent); - } - - // Recursive version... - // if (address === undefined && METADATA in target) { - // const parent: object | null = Reflect.getPrototypeOf(target); - // - // if (parent !== null && parent !== Reflect.getPrototypeOf(Function)) { - // return findAddress(parent); - // } - // } - - return address; -} - -/** - * Save metadata address in internal registry, for given target - * - * @param {object} target The target metadata is to be associated with - * @param {MetaAddress} address Location where actual metadata is to be found - */ -function saveAddress(target: object, address: MetaAddress): void -{ - addressesRegistry.set(target, address); -} - -/** - * Returns a "prefix" key (path) where "target" metadata must be stored - * - * @param {Context} context - * - * @return {Key} - * - * @throws {TypeError} If {@link Context.kind} is not supported - */ -function makePrefixKey(context: Context): Key -{ - if (!Reflect.has(Kind, context.kind)) { - throw new TypeError(`context.kind: "${context.kind}" is unsupported`); - } - - // Debug - // console.log('@kind', ELEMENT_KIND_MAP[Kind[context.kind]]); - - const output: PropertyKey[] = [ - TARGET_METADATA, - ELEMENT_KIND_IDENTIFIERS[Kind[context.kind]] - ]; - - // Ensures that we do not overwrite static / none-static elements with same name! - if (context.kind !== 'class' && context.static) { - output.push(STATIC_IDENTIFIER); - } - - // "anonymous" is for anonymous classes (they do not have a name) - const name: string | symbol = context.name ?? 'anonymous'; - output.push(name); - - return output as Key; -} - -/** - * Returns a new metadata entry - * - * @param {object} target - * @param {Context} context - * @param {object} owner - * @param {Key} prefixKey - * @param {Key|MetaCallback} key User provided key or callback - * @param {unknown} [value] Value to store. Ignored if `key` argument is - * a callback. - * - * @return {MetaEntry} - */ -function makeMetaEntry( - target: object, - context: Context, - owner: object, - prefixKey: Key, - key: Key | MetaCallback, - value?: unknown -): MetaEntry -{ - let resolvedKey: Key | MetaCallback = key; - let resolvedValue: unknown = value; - - // When key is a callback, invoke it and use its resulting key-value pair. - if (typeof key == 'function') { - const entry: MetaEntry = (key as MetaCallback)(target, context, owner); - - resolvedKey = entry.key; - resolvedValue = entry.value; - } - - return { - key: mergeKeys(prefixKey, resolvedKey as Key), - value: resolvedValue - } as MetaEntry; -} \ No newline at end of file diff --git a/tests/browser/packages/support/meta/inheritTargetMeta.test.js b/tests/browser/packages/support/meta/inheritTargetMeta.test.js index 69dd17ab..b32eca36 100644 --- a/tests/browser/packages/support/meta/inheritTargetMeta.test.js +++ b/tests/browser/packages/support/meta/inheritTargetMeta.test.js @@ -1,7 +1,8 @@ import { targetMeta, getTargetMeta, - inheritTargetMeta + inheritTargetMeta, + MetaError } from "@aedart/support/meta"; describe('@aedart/support/meta', () => { @@ -51,7 +52,7 @@ describe('@aedart/support/meta', () => { expect(callback) .withContext('@inheritTargetMeta() should not be allowed on base class') - .toThrowError(TypeError); + .toThrowError(MetaError); }); it('fails when @inheritTargetMeta() used on base method', () => { @@ -71,7 +72,7 @@ describe('@aedart/support/meta', () => { expect(callback) .withContext('@inheritTargetMeta() should not be allowed on base method') - .toThrowError(TypeError); + .toThrowError(MetaError); }); it('inherits target static method meta, via @inheritTargetMeta()', () => { @@ -124,7 +125,7 @@ describe('@aedart/support/meta', () => { expect(callback) .withContext('@inheritTargetMeta() should fail when nothing to inherit') - .toThrowError(TypeError); + .toThrowError(MetaError); }); }); }); \ No newline at end of file diff --git a/tests/browser/packages/support/meta/targetMeta.test.js b/tests/browser/packages/support/meta/targetMeta.test.js index ebabbb40..0aef834a 100644 --- a/tests/browser/packages/support/meta/targetMeta.test.js +++ b/tests/browser/packages/support/meta/targetMeta.test.js @@ -1,6 +1,8 @@ import { targetMeta, getTargetMeta, + hasTargetMeta, + MetaError } from "@aedart/support/meta"; describe('@aedart/support/meta', () => { @@ -16,7 +18,11 @@ describe('@aedart/support/meta', () => { class A {} // ---------------------------------------------------------------------- // - + + expect(hasTargetMeta(A, key)) + .withContext('Target does not appear to have meta') + .toBeTrue(); + const result = getTargetMeta(A, key); expect(result) .withContext('Incorrect target meta') @@ -95,8 +101,13 @@ describe('@aedart/support/meta', () => { // ---------------------------------------------------------------------- // const instance = new A(); - const result = getTargetMeta(instance.foo, key); + const target = instance.foo; + + expect(hasTargetMeta(target, key)) + .withContext('Target does not appear to have meta for method') + .toBeTrue(); + const result = getTargetMeta(target, key); expect(result) .withContext('Incorrect method target meta') .toEqual(value); @@ -303,7 +314,7 @@ describe('@aedart/support/meta', () => { expect(callback) .withContext('Should not support @targetMeta on unsupported element') - .toThrowError(TypeError); + .toThrowError(MetaError); }); });