From 2fac706d0037889f6eedfd67854444e83ddf5ab3 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 08:52:59 +0100 Subject: [PATCH 01/36] Remove deprecated components --- .../src/support/meta/AccessorContext.ts | 9 --------- .../contracts/src/support/meta/ClassContext.ts | 9 --------- .../contracts/src/support/meta/FieldContext.ts | 9 --------- .../src/support/meta/GetterContext.ts | 9 --------- .../src/support/meta/MetadataContext.ts | 16 ---------------- .../src/support/meta/MethodContext.ts | 9 --------- .../src/support/meta/SetterContext.ts | 9 --------- packages/contracts/src/support/meta/index.ts | 15 --------------- packages/contracts/src/support/meta/types.ts | 18 ------------------ 9 files changed, 103 deletions(-) delete mode 100644 packages/contracts/src/support/meta/AccessorContext.ts delete mode 100644 packages/contracts/src/support/meta/ClassContext.ts delete mode 100644 packages/contracts/src/support/meta/FieldContext.ts delete mode 100644 packages/contracts/src/support/meta/GetterContext.ts delete mode 100644 packages/contracts/src/support/meta/MetadataContext.ts delete mode 100644 packages/contracts/src/support/meta/MethodContext.ts delete mode 100644 packages/contracts/src/support/meta/SetterContext.ts 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/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/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/index.ts b/packages/contracts/src/support/meta/index.ts index 91815591..35ddcd81 100644 --- a/packages/contracts/src/support/meta/index.ts +++ b/packages/contracts/src/support/meta/index.ts @@ -1,10 +1,3 @@ -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 Kind from "./Kind"; @@ -32,14 +25,6 @@ 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, diff --git a/packages/contracts/src/support/meta/types.ts b/packages/contracts/src/support/meta/types.ts index 1d943e1b..8a9f54d3 100644 --- a/packages/contracts/src/support/meta/types.ts +++ b/packages/contracts/src/support/meta/types.ts @@ -1,8 +1,3 @@ -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"; @@ -11,19 +6,6 @@ import type { Key } from "@aedart/contracts/support"; */ 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. */ From 7fa9ce11bc3345e44ef6b5f7e6816bf6c0a95015 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 08:53:06 +0100 Subject: [PATCH 02/36] Change release notes --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8e9901e..5087f852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * 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 From 2354d9caece09bc18b9ebdc54015a22aa94689d4 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 09:36:10 +0100 Subject: [PATCH 03/36] Add general meta exception interface --- .../src/support/meta/exceptions/MetaException.ts | 8 ++++++++ packages/contracts/src/support/meta/exceptions/index.ts | 4 ++++ packages/contracts/src/support/meta/index.ts | 1 + 3 files changed, 13 insertions(+) create mode 100644 packages/contracts/src/support/meta/exceptions/MetaException.ts create mode 100644 packages/contracts/src/support/meta/exceptions/index.ts 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 35ddcd81..c5ffcd13 100644 --- a/packages/contracts/src/support/meta/index.ts +++ b/packages/contracts/src/support/meta/index.ts @@ -31,4 +31,5 @@ export { Kind }; +export * from './exceptions/index'; export type * from './types'; \ No newline at end of file From 3e3fa8d9f3b0bcf137ebbd3cf91511a57471c5ff Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 09:37:39 +0100 Subject: [PATCH 04/36] Add meta repository interface --- .../contracts/src/support/meta/Repository.ts | 67 +++++++++++++++++++ packages/contracts/src/support/meta/index.ts | 2 + 2 files changed, 69 insertions(+) create mode 100644 packages/contracts/src/support/meta/Repository.ts diff --git a/packages/contracts/src/support/meta/Repository.ts b/packages/contracts/src/support/meta/Repository.ts new file mode 100644 index 00000000..cdf07f9d --- /dev/null +++ b/packages/contracts/src/support/meta/Repository.ts @@ -0,0 +1,67 @@ +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; + + /** + * Decorator context + * + * @type {Context|undefined} + */ + readonly context?: Context + + /** + * Set value for given key + * + * @param {Key | MetaCallback} key + * @param {any} [value] Value to be stored. Ignored if `key` argument is a callback. + * + * @return {void} + * + * @throws {MetaException} + */ + set( + key: Key | MetaCallback, + value?: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): void; + + /** + * 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 + */ + 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/index.ts b/packages/contracts/src/support/meta/index.ts index c5ffcd13..fbdb3a4e 100644 --- a/packages/contracts/src/support/meta/index.ts +++ b/packages/contracts/src/support/meta/index.ts @@ -1,5 +1,6 @@ import MetaEntry from "./MetaEntry"; import MetaTargetContext from "./MetaTargetContext"; +import Repository from "./Repository"; import Kind from "./Kind"; /** @@ -27,6 +28,7 @@ export const TARGET_METADATA: unique symbol = Symbol('target_metadata'); export { type MetaEntry, type MetaTargetContext, + type Repository, Kind }; From 9e4773b0ecf58a555cad32c89ee30c2403663ab2 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 09:38:05 +0100 Subject: [PATCH 05/36] Fix style --- packages/contracts/src/support/meta/Repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/meta/Repository.ts b/packages/contracts/src/support/meta/Repository.ts index cdf07f9d..eb6098a1 100644 --- a/packages/contracts/src/support/meta/Repository.ts +++ b/packages/contracts/src/support/meta/Repository.ts @@ -1,5 +1,5 @@ import { Key } from "@aedart/contracts/support"; -import {Context, MetaCallback, MetadataRecord} from "./types"; +import { Context, MetaCallback, MetadataRecord } from "./types"; /** * Meta Repository From a2c4a0bc38fc45cc0229ca594ce4410ac72f2e4c Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 09:42:09 +0100 Subject: [PATCH 06/36] Add Meta Error --- .../support/src/meta/exceptions/MetaError.ts | 23 +++++++++++++++++++ packages/support/src/meta/exceptions/index.ts | 4 ++++ packages/support/src/meta/index.ts | 1 + 3 files changed, 28 insertions(+) create mode 100644 packages/support/src/meta/exceptions/MetaError.ts create mode 100644 packages/support/src/meta/exceptions/index.ts 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/index.ts b/packages/support/src/meta/index.ts index 4a73eb1c..807767d8 100644 --- a/packages/support/src/meta/index.ts +++ b/packages/support/src/meta/index.ts @@ -1,2 +1,3 @@ +export * from './exceptions'; export * from './meta'; export * from './targetMeta'; \ No newline at end of file From 92f09db87f076db3cdf4fc47edd9e9cfcfca0c1a Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 10:56:51 +0100 Subject: [PATCH 07/36] Change return type of set method Now it should correspond to the value that a decorator will return. --- packages/contracts/src/support/meta/Repository.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/meta/Repository.ts b/packages/contracts/src/support/meta/Repository.ts index eb6098a1..b20f3fb3 100644 --- a/packages/contracts/src/support/meta/Repository.ts +++ b/packages/contracts/src/support/meta/Repository.ts @@ -26,14 +26,14 @@ export default interface Repository * @param {Key | MetaCallback} key * @param {any} [value] Value to be stored. Ignored if `key` argument is a callback. * - * @return {void} + * @return {void | ((initialValue: unknown) => unknown) | undefined} * * @throws {MetaException} */ set( key: Key | MetaCallback, value?: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ - ): void; + ): void | ((initialValue: unknown) => unknown) | undefined /** * Get value for given key From 2d96a9bd0b9e5f3f6847523213cf2b7b827dfc3b Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 11:17:45 +0100 Subject: [PATCH 08/36] Change set, adapt to fit decorator usage Well... it seems that both target and context must be specified this way, to ensure that previous solution is respected. Not sure if this should be abstracted away from the Repository. --- packages/contracts/src/support/meta/Repository.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/contracts/src/support/meta/Repository.ts b/packages/contracts/src/support/meta/Repository.ts index b20f3fb3..183e54cf 100644 --- a/packages/contracts/src/support/meta/Repository.ts +++ b/packages/contracts/src/support/meta/Repository.ts @@ -13,16 +13,13 @@ export default interface Repository */ readonly owner: object; - /** - * Decorator context - * - * @type {Context|undefined} - */ - readonly context?: Context - /** * 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. * @@ -31,6 +28,8 @@ export default interface Repository * @throws {MetaException} */ set( + target: object, + context: Context, key: Key | MetaCallback, value?: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ ): void | ((initialValue: unknown) => unknown) | undefined From c572a7bcd6c0a94ffb6e3a27cdf4339d5a25ee37 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 11:25:20 +0100 Subject: [PATCH 09/36] Add InitializerCallback type alias --- packages/contracts/src/support/meta/types.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/contracts/src/support/meta/types.ts b/packages/contracts/src/support/meta/types.ts index 8a9f54d3..35e98960 100644 --- a/packages/contracts/src/support/meta/types.ts +++ b/packages/contracts/src/support/meta/types.ts @@ -21,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 */ From 296d6552bc110912cc8a413ca97747c3888aafa5 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 13:05:33 +0100 Subject: [PATCH 10/36] Change set, remove exception thrown --- packages/contracts/src/support/meta/Repository.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/contracts/src/support/meta/Repository.ts b/packages/contracts/src/support/meta/Repository.ts index 183e54cf..009c1991 100644 --- a/packages/contracts/src/support/meta/Repository.ts +++ b/packages/contracts/src/support/meta/Repository.ts @@ -24,8 +24,6 @@ export default interface Repository * @param {any} [value] Value to be stored. Ignored if `key` argument is a callback. * * @return {void | ((initialValue: unknown) => unknown) | undefined} - * - * @throws {MetaException} */ set( target: object, From 46634db69ffdabf676651a7595ebd1e4f019cf46 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 13:42:40 +0100 Subject: [PATCH 11/36] Add decorator types --- packages/contracts/src/types.ts | 56 ++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/types.ts b/packages/contracts/src/types.ts index 1cf6b933..308adec9 100644 --- a/packages/contracts/src/types.ts +++ b/packages/contracts/src/types.ts @@ -18,4 +18,58 @@ export type AbstractConstructor = abstract new (...args: any[]) => T /** * Constructor or Abstract Constructor type */ -export type ConstructorOrAbstractConstructor = Constructor | AbstractConstructor; \ No newline at end of file +export type ConstructorOrAbstractConstructor = Constructor | AbstractConstructor; + +/** + * Class Decorator + * + * @see https://github.com/tc39/proposal-decorators + */ +export type ClassDecorator = (target: object, context: ClassDecoratorContext) => object | void; + +/** + * Class Method Decorator + * + * @see https://github.com/tc39/proposal-decorators + */ +export type ClassMethodDecorator = (target: object, context: ClassMethodDecoratorContext) => object | void; + +/** + * Class Getter Decorator + * + * @see https://github.com/tc39/proposal-decorators + */ +export type ClassGetterDecorator = (target: object, context: ClassGetterDecoratorContext) => object | void; + +/** + * Class Setter Decorator + * + * @see https://github.com/tc39/proposal-decorators + */ +export type ClassSetterDecorator = (target: object, context: ClassSetterDecoratorContext) => object | void; + +/** + * Class Field Decorator + * + * @see https://github.com/tc39/proposal-decorators + */ +export type ClassFieldDecorator = (target: object, context: ClassFieldDecoratorContext) => (initialValue: unknown) => unknown | void; + +/** + * Class Auto-Accessor Decorator + * + * @see https://github.com/tc39/proposal-decorators + */ +export type ClassAutoAccessorDecorator = (target: ClassAccessorDecoratorTarget, context: ClassAccessorDecoratorContext) => ClassAccessorDecoratorResult | void + +/** + * Decorator + * + * @see https://github.com/tc39/proposal-decorators + */ +export type Decorator = ClassDecorator + | ClassMethodDecorator + | ClassGetterDecorator + | ClassSetterDecorator + | ClassFieldDecorator + | ClassAutoAccessorDecorator; \ No newline at end of file From 78bc536a9515583e7e90b5ba04b4be6aadc1963c Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 13:52:34 +0100 Subject: [PATCH 12/36] Add decorator result types This should make it easier to describe the return value(s) for decorator related utils. --- packages/contracts/src/types.ts | 52 +++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/packages/contracts/src/types.ts b/packages/contracts/src/types.ts index 308adec9..c52180b7 100644 --- a/packages/contracts/src/types.ts +++ b/packages/contracts/src/types.ts @@ -20,47 +20,87 @@ export type AbstractConstructor = abstract new (...args: any[]) => T */ export type ConstructorOrAbstractConstructor = Constructor | AbstractConstructor; +/** + * 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) => object | void; +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) => object | void; +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) => object | void; +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) => object | void; +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) => (initialValue: unknown) => unknown | void; +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) => ClassAccessorDecoratorResult | void +export type ClassAutoAccessorDecorator = (target: ClassAccessorDecoratorTarget, context: ClassAccessorDecoratorContext) => ClassAutoAccessorDecoratorResult; /** * Decorator From 4556c4e0ab940f18174c8a0d40b8901934dbf249 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 14:36:12 +0100 Subject: [PATCH 13/36] Extract decorator types into own file --- packages/contracts/src/decorators.ts | 93 ++++++++++++++++++++++++++ packages/contracts/src/types.ts | 98 +--------------------------- 2 files changed, 96 insertions(+), 95 deletions(-) create mode 100644 packages/contracts/src/decorators.ts 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/types.ts b/packages/contracts/src/types.ts index c52180b7..b19e0e14 100644 --- a/packages/contracts/src/types.ts +++ b/packages/contracts/src/types.ts @@ -1,3 +1,5 @@ +export * from './decorators'; + /** * Primitive value * @@ -18,98 +20,4 @@ export type AbstractConstructor = abstract new (...args: any[]) => T /** * Constructor or Abstract Constructor type */ -export type ConstructorOrAbstractConstructor = Constructor | AbstractConstructor; - -/** - * 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 +export type ConstructorOrAbstractConstructor = Constructor | AbstractConstructor; \ No newline at end of file From e2bce8a5c65a6f2b61cd84245224918390a35aa3 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 14:41:20 +0100 Subject: [PATCH 14/36] Fix style --- packages/contracts/src/support/meta/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/meta/types.ts b/packages/contracts/src/support/meta/types.ts index 35e98960..ab4639a1 100644 --- a/packages/contracts/src/support/meta/types.ts +++ b/packages/contracts/src/support/meta/types.ts @@ -1,5 +1,5 @@ -import MetaEntry from "./MetaEntry"; import type { Key } from "@aedart/contracts/support"; +import MetaEntry from "./MetaEntry"; /** * Decorator context types for any decorator From f6015953852d48d0af2788295690bdba5cf5b82c Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 14:58:39 +0100 Subject: [PATCH 15/36] Change return type of set to DecoratorResult --- packages/contracts/src/support/meta/Repository.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/support/meta/Repository.ts b/packages/contracts/src/support/meta/Repository.ts index 009c1991..d1ab6f38 100644 --- a/packages/contracts/src/support/meta/Repository.ts +++ b/packages/contracts/src/support/meta/Repository.ts @@ -1,3 +1,4 @@ +import { DecoratorResult } from "@aedart/contracts"; import { Key } from "@aedart/contracts/support"; import { Context, MetaCallback, MetadataRecord } from "./types"; @@ -30,7 +31,7 @@ export default interface Repository context: Context, key: Key | MetaCallback, value?: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ - ): void | ((initialValue: unknown) => unknown) | undefined + ): DecoratorResult; /** * Get value for given key From db51ed5f30ee605b07bfce23858e2ac0cd6f8024 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 14:59:43 +0100 Subject: [PATCH 16/36] Add context as part of target context This will allow reducing the "context" parameter for some of the internal helper methods. --- packages/contracts/src/support/meta/MetaTargetContext.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/contracts/src/support/meta/MetaTargetContext.ts b/packages/contracts/src/support/meta/MetaTargetContext.ts index 3db816a7..2ceb6592 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 */ @@ -17,4 +19,9 @@ export default interface MetaTargetContext * The target class, field, method... that is being decorated */ target: object, + + /** + * Decorator context + */ + context: Context, } \ No newline at end of file From 34775c0dd8b69d394fe92abd8500aac7cfd6888c Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 15:01:34 +0100 Subject: [PATCH 17/36] Refactor meta(), extract logic into Meta Repository A new Meta Repository class is now responsible for reading and writing metadata. This should make it a lot easier for other developers to create their own "meta" version, if needed. --- packages/support/src/meta/MetaRepository.ts | 380 ++++++++++++++++++ packages/support/src/meta/getAllMeta.ts | 16 + packages/support/src/meta/getMeta.ts | 21 + .../support/src/meta/getMetaRepository.ts | 14 + packages/support/src/meta/index.ts | 13 +- packages/support/src/meta/meta.ts | 318 +-------------- packages/support/src/meta/targetMeta.ts | 38 +- 7 files changed, 476 insertions(+), 324 deletions(-) create mode 100644 packages/support/src/meta/MetaRepository.ts create mode 100644 packages/support/src/meta/getAllMeta.ts create mode 100644 packages/support/src/meta/getMeta.ts create mode 100644 packages/support/src/meta/getMetaRepository.ts diff --git a/packages/support/src/meta/MetaRepository.ts b/packages/support/src/meta/MetaRepository.ts new file mode 100644 index 00000000..c3f36dd4 --- /dev/null +++ b/packages/support/src/meta/MetaRepository.ts @@ -0,0 +1,380 @@ +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"; + +/** + * 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 + { + if (typeof key === 'function') { + return (key as MetaCallback)(targetContext.target, targetContext.context, targetContext.owner); + } + + return { + key: (key as Key), + value: 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 + { + // 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 { + // owner: this.resolveTargetOwner(thisArg, context), + owner: owner, + thisArg: thisArg, + target: target, + context: context + } as MetaTargetContext; + } +} \ 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/index.ts b/packages/support/src/meta/index.ts index 807767d8..a2cc0729 100644 --- a/packages/support/src/meta/index.ts +++ b/packages/support/src/meta/index.ts @@ -1,3 +1,12 @@ -export * from './exceptions'; +import MetaRepository from "./MetaRepository"; +export { + MetaRepository, +} + +export * from './getMetaRepository'; +export * from './getAllMeta'; +export * from './getMeta'; export * from './meta'; -export * from './targetMeta'; \ No newline at end of file +export * from './targetMeta'; + +export * from './exceptions'; \ 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/targetMeta.ts b/packages/support/src/meta/targetMeta.ts index a89390a5..38fc6a88 100644 --- a/packages/support/src/meta/targetMeta.ts +++ b/packages/support/src/meta/targetMeta.ts @@ -1,3 +1,7 @@ +import type { + ClassDecorator, + ClassMethodDecorator +} from "@aedart/contracts"; import type { Key } from "@aedart/contracts/support"; import type { Context, @@ -8,7 +12,8 @@ import type { 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' +import { meta } from "./meta"; +import { getMeta } from "./getMeta"; /** * Registry that contains the target object (e.g. a class or a method), @@ -59,21 +64,22 @@ const STATIC_IDENTIFIER: unique symbol = Symbol('static'); * 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)} + * @returns {ClassDecorator | ClassMethodDecorator} * * @throws {TypeError} When decorated element is not supported */ export function targetMeta( key: Key | MetaCallback, value?: unknown -) { +): ClassDecorator | ClassMethodDecorator +{ 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); @@ -81,7 +87,7 @@ export function targetMeta( new WeakRef(owner), prefixKey ]; - + // Save the address in the registry... saveAddress(target, address); @@ -97,7 +103,7 @@ export function targetMeta( saveAddress(proto[context.name], address); } } - + // Finally, return the meta key-value pair that will be stored in the owner's metadata. return makeMetaEntry( target, @@ -107,7 +113,7 @@ export function targetMeta( key, value ); - }); + }) as ClassDecorator | ClassMethodDecorator; } /** @@ -155,12 +161,12 @@ export function inheritTargetMeta() // 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]; @@ -240,7 +246,7 @@ function findAddress(target: object): MetaAddress | undefined if (addressesRegistry.has(target.constructor)) { return addressesRegistry.get(target.constructor); } - + // Otherwise, change the target to the constructor. target = target.constructor; } @@ -266,7 +272,7 @@ function findAddress(target: object): MetaAddress | undefined // return findAddress(parent); // } // } - + return address; } @@ -295,10 +301,10 @@ 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]] @@ -312,7 +318,7 @@ function makePrefixKey(context: Context): Key // "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; } @@ -340,7 +346,7 @@ function makeMetaEntry( { 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); @@ -348,7 +354,7 @@ function makeMetaEntry( resolvedKey = entry.key; resolvedValue = entry.value; } - + return { key: mergeKeys(prefixKey, resolvedKey as Key), value: resolvedValue From 51e8a093086322676766f16569141d55ad1cb670 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 15:04:56 +0100 Subject: [PATCH 18/36] Add hasMeta util function --- packages/support/src/meta/hasMeta.ts | 15 +++++++++++++++ packages/support/src/meta/index.ts | 1 + 2 files changed, 16 insertions(+) create mode 100644 packages/support/src/meta/hasMeta.ts 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 a2cc0729..256bbed5 100644 --- a/packages/support/src/meta/index.ts +++ b/packages/support/src/meta/index.ts @@ -4,6 +4,7 @@ export { } export * from './getMetaRepository'; +export * from './hasMeta'; export * from './getAllMeta'; export * from './getMeta'; export * from './meta'; From e2c36a1d927b42730c72318a96a4a6935011b54b Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 15:05:07 +0100 Subject: [PATCH 19/36] Change release notes --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5087f852..3370233d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +* `hasMeta()` util, in `@aedart/support/meta`. +* `MetaRepository` 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 + +* Refactored / Redesigned `meta()`, `getMeta()`, and `getAllMeta()` to use new `MetaRepository` as its underlying core component for dealing with metadata. +* 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. From 738be6b93ffed2b22b6544fdf0b99816a3d67c3e Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 15:15:46 +0100 Subject: [PATCH 20/36] Fix return type (JSDoc) --- packages/contracts/src/support/meta/Repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/meta/Repository.ts b/packages/contracts/src/support/meta/Repository.ts index d1ab6f38..af7fb16f 100644 --- a/packages/contracts/src/support/meta/Repository.ts +++ b/packages/contracts/src/support/meta/Repository.ts @@ -24,7 +24,7 @@ export default interface Repository * @param {Key | MetaCallback} key * @param {any} [value] Value to be stored. Ignored if `key` argument is a callback. * - * @return {void | ((initialValue: unknown) => unknown) | undefined} + * @return {DecoratorResult} */ set( target: object, From 529585e6a559382bf4b6d8c411d2f5f57ed93090 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 15:21:52 +0100 Subject: [PATCH 21/36] Fix missing return type for has() - JSDoc --- packages/contracts/src/support/meta/Repository.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contracts/src/support/meta/Repository.ts b/packages/contracts/src/support/meta/Repository.ts index af7fb16f..c5bee737 100644 --- a/packages/contracts/src/support/meta/Repository.ts +++ b/packages/contracts/src/support/meta/Repository.ts @@ -53,6 +53,8 @@ export default interface Repository * Determine if value exists for key * * @param {Key} key + * + * @return {boolean} */ has(key: Key): boolean; From 0bf3a9b0ba9d6ea395d3c1b87c12040316b33cb7 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 6 Mar 2024 15:23:27 +0100 Subject: [PATCH 22/36] Add Target Repository interface --- .../src/support/meta/TargetRepository.ts | 59 +++++++++++++++++++ packages/contracts/src/support/meta/index.ts | 2 + 2 files changed, 61 insertions(+) create mode 100644 packages/contracts/src/support/meta/TargetRepository.ts diff --git a/packages/contracts/src/support/meta/TargetRepository.ts b/packages/contracts/src/support/meta/TargetRepository.ts new file mode 100644 index 00000000..13ae24bc --- /dev/null +++ b/packages/contracts/src/support/meta/TargetRepository.ts @@ -0,0 +1,59 @@ +import { Key } from "@aedart/contracts/support"; +import { Context, MetaCallback } from "@aedart/contracts/support/meta/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; +} \ 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 fbdb3a4e..55646bf3 100644 --- a/packages/contracts/src/support/meta/index.ts +++ b/packages/contracts/src/support/meta/index.ts @@ -1,6 +1,7 @@ import MetaEntry from "./MetaEntry"; import MetaTargetContext from "./MetaTargetContext"; import Repository from "./Repository"; +import TargetRepository from "./TargetRepository"; import Kind from "./Kind"; /** @@ -29,6 +30,7 @@ export { type MetaEntry, type MetaTargetContext, type Repository, + type TargetRepository, Kind }; From 1b7cbb609f96c69ee1b69621b0986a69cb116903 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 09:34:38 +0100 Subject: [PATCH 23/36] Improve property descriptions --- .../src/support/meta/MetaTargetContext.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/contracts/src/support/meta/MetaTargetContext.ts b/packages/contracts/src/support/meta/MetaTargetContext.ts index 2ceb6592..b33eb7e3 100644 --- a/packages/contracts/src/support/meta/MetaTargetContext.ts +++ b/packages/contracts/src/support/meta/MetaTargetContext.ts @@ -7,21 +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, + target: object; /** * Decorator context + * + * @type {Context} */ - context: Context, + context: Context; } \ No newline at end of file From 4adf770f8b45f7e8eea77138af296d0efd137e37 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 09:44:37 +0100 Subject: [PATCH 24/36] Fix import --- packages/contracts/src/support/meta/TargetRepository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/meta/TargetRepository.ts b/packages/contracts/src/support/meta/TargetRepository.ts index 13ae24bc..4effac6f 100644 --- a/packages/contracts/src/support/meta/TargetRepository.ts +++ b/packages/contracts/src/support/meta/TargetRepository.ts @@ -1,5 +1,5 @@ import { Key } from "@aedart/contracts/support"; -import { Context, MetaCallback } from "@aedart/contracts/support/meta/types"; +import { Context, MetaCallback } from "./types"; import { ClassDecoratorResult, ClassMethodDecoratorResult } from "@aedart/contracts"; /** From b94a03453ef4dc8c64703caaf1bf4e0581844a89 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 09:45:50 +0100 Subject: [PATCH 25/36] Add Meta Target Context --- packages/support/src/meta/TargetContext.ts | 82 ++++++++++++++++++++++ packages/support/src/meta/index.ts | 5 +- 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 packages/support/src/meta/TargetContext.ts diff --git a/packages/support/src/meta/TargetContext.ts b/packages/support/src/meta/TargetContext.ts new file mode 100644 index 00000000..ba728c97 --- /dev/null +++ b/packages/support/src/meta/TargetContext.ts @@ -0,0 +1,82 @@ +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 + } + + /** + * Resolves target's owner and returns a new Meta Target Instance + * + * @param {object} target + * @param {object} thisArg + * @param {Context} context + * + * @return {this|MetaTargetContext} + */ + 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 new this(owner, thisArg, target, context); + } +} \ No newline at end of file diff --git a/packages/support/src/meta/index.ts b/packages/support/src/meta/index.ts index 256bbed5..f28f2457 100644 --- a/packages/support/src/meta/index.ts +++ b/packages/support/src/meta/index.ts @@ -1,6 +1,8 @@ import MetaRepository from "./MetaRepository"; +import TargetContext from "./TargetContext"; export { MetaRepository, + TargetContext, } export * from './getMetaRepository'; @@ -10,4 +12,5 @@ export * from './getMeta'; export * from './meta'; export * from './targetMeta'; -export * from './exceptions'; \ No newline at end of file +export * from './exceptions'; +export * from './target/index' \ No newline at end of file From 1cbd15e9a3a57dd73779663fab0931dee9b57188 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 09:47:09 +0100 Subject: [PATCH 26/36] Refactor, use new target context --- packages/support/src/meta/MetaRepository.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/support/src/meta/MetaRepository.ts b/packages/support/src/meta/MetaRepository.ts index c3f36dd4..9ab6bb67 100644 --- a/packages/support/src/meta/MetaRepository.ts +++ b/packages/support/src/meta/MetaRepository.ts @@ -11,6 +11,7 @@ import type { import { METADATA } from "@aedart/contracts/support/meta"; import type { Key } from "@aedart/contracts/support"; import { set, get, has, merge } from "@aedart/support/objects"; +import TargetContext from "./TargetContext"; /** * Fallback registry that contains writable metadata (`context.metadata`). @@ -362,19 +363,6 @@ export default class MetaRepository implements Repository 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 { - // owner: this.resolveTargetOwner(thisArg, context), - owner: owner, - thisArg: thisArg, - target: target, - context: context - } as MetaTargetContext; + return TargetContext.resolveOwner(target, thisArg, context); } } \ No newline at end of file From 755d986acddbb72fcfb37bc118e001947d583f3d Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 09:50:38 +0100 Subject: [PATCH 27/36] Cleanup --- packages/contracts/src/support/meta/MetaEntry.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From e4aba0c1253175fa7f8d0c330747727a43ba4545 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 09:57:20 +0100 Subject: [PATCH 28/36] Add Meta Entry class --- packages/support/src/meta/Entry.ts | 55 ++++++++++++++++++++++++++++++ packages/support/src/meta/index.ts | 2 ++ 2 files changed, 57 insertions(+) create mode 100644 packages/support/src/meta/Entry.ts diff --git a/packages/support/src/meta/Entry.ts b/packages/support/src/meta/Entry.ts new file mode 100644 index 00000000..c0a71bd5 --- /dev/null +++ b/packages/support/src/meta/Entry.ts @@ -0,0 +1,55 @@ +import type { Key } from "@aedart/contracts/support"; +import type { MetaCallback, MetaEntry, MetaTargetContext } from "@aedart/contracts/support/meta"; + +/** + * 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; + } + + /** + * Resolves given key and returns a new Meta Entry instance + * + * @param {MetaTargetContext} targetContext + * @param {Key | MetaCallback} key + * @param {any} [value] + */ + 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 new this(key as Key, value); + } +} \ No newline at end of file diff --git a/packages/support/src/meta/index.ts b/packages/support/src/meta/index.ts index f28f2457..fa613162 100644 --- a/packages/support/src/meta/index.ts +++ b/packages/support/src/meta/index.ts @@ -1,6 +1,8 @@ +import Entry from "./Entry"; import MetaRepository from "./MetaRepository"; import TargetContext from "./TargetContext"; export { + Entry, MetaRepository, TargetContext, } From 4e15dc2ba8b6ef251a5c72533d77451fecb351d0 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 09:57:37 +0100 Subject: [PATCH 29/36] Refactor, use new Meta Entry --- packages/support/src/meta/MetaRepository.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/support/src/meta/MetaRepository.ts b/packages/support/src/meta/MetaRepository.ts index 9ab6bb67..90cc0b85 100644 --- a/packages/support/src/meta/MetaRepository.ts +++ b/packages/support/src/meta/MetaRepository.ts @@ -11,6 +11,7 @@ import type { 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"; /** @@ -333,14 +334,7 @@ export default class MetaRepository implements Repository 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 { - key: (key as Key), - value: value - }; + return Entry.resolve(targetContext, key, value); } /** From d37d0202289b162aa3bfb83568ba78116b941ba6 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 09:59:03 +0100 Subject: [PATCH 30/36] Fix JSDoc --- packages/support/src/meta/Entry.ts | 4 ++++ packages/support/src/meta/TargetContext.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/packages/support/src/meta/Entry.ts b/packages/support/src/meta/Entry.ts index c0a71bd5..5cc85f57 100644 --- a/packages/support/src/meta/Entry.ts +++ b/packages/support/src/meta/Entry.ts @@ -39,6 +39,10 @@ export default class Entry implements MetaEntry * @param {MetaTargetContext} targetContext * @param {Key | MetaCallback} key * @param {any} [value] + * + * @return {this|MetaEntry} + * + * @static */ public static resolve( targetContext: MetaTargetContext, diff --git a/packages/support/src/meta/TargetContext.ts b/packages/support/src/meta/TargetContext.ts index ba728c97..26a12791 100644 --- a/packages/support/src/meta/TargetContext.ts +++ b/packages/support/src/meta/TargetContext.ts @@ -63,6 +63,8 @@ export default class TargetContext implements MetaTargetContext * @param {Context} context * * @return {this|MetaTargetContext} + * + * @static */ public static resolveOwner( target: object, From 2dcf04372a308a9aefa4ee4c95997ed61f07ddd5 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 10:51:30 +0100 Subject: [PATCH 31/36] Add inherit method To be used by the "inheritTargetMeta()" util. --- .../src/support/meta/TargetRepository.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/contracts/src/support/meta/TargetRepository.ts b/packages/contracts/src/support/meta/TargetRepository.ts index 4effac6f..7456a36b 100644 --- a/packages/contracts/src/support/meta/TargetRepository.ts +++ b/packages/contracts/src/support/meta/TargetRepository.ts @@ -56,4 +56,21 @@ export default interface TargetRepository * @return {boolean} */ has(target: object, key: Key): boolean; + + /** + * 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._ + * + * @param {object} target + * @param {Context} context + * + * @return {ClassMethodDecoratorResult} + * + * @throws {MetaException} + */ + inherit(target: object, context: Context): ClassMethodDecoratorResult; } \ No newline at end of file From 87efda1dc302d09f336e840e159b524b062c7e57 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 11:40:52 +0100 Subject: [PATCH 32/36] Change Entry, add static make method and resolve with prefix --- packages/support/src/meta/Entry.ts | 44 +++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/support/src/meta/Entry.ts b/packages/support/src/meta/Entry.ts index 5cc85f57..d90b92e5 100644 --- a/packages/support/src/meta/Entry.ts +++ b/packages/support/src/meta/Entry.ts @@ -1,5 +1,6 @@ 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 @@ -33,6 +34,21 @@ export default class Entry implements MetaEntry 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 * @@ -54,6 +70,32 @@ export default class Entry implements MetaEntry return (key as MetaCallback)(targetContext.target, targetContext.context, targetContext.owner); } - return new this(key as Key, value); + 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 From d81021b15b96ac23d15c91b56ca1418ebd285277 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 11:41:15 +0100 Subject: [PATCH 33/36] Change Target Context, add static make method --- packages/support/src/meta/TargetContext.ts | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/support/src/meta/TargetContext.ts b/packages/support/src/meta/TargetContext.ts index 26a12791..14528dca 100644 --- a/packages/support/src/meta/TargetContext.ts +++ b/packages/support/src/meta/TargetContext.ts @@ -55,6 +55,28 @@ export default class TargetContext implements MetaTargetContext 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 * @@ -79,6 +101,6 @@ export default class TargetContext implements MetaTargetContext // When target is not static, then it's obtainable via prototype : (Reflect.getPrototypeOf(thisArg) as object).constructor; - return new this(owner, thisArg, target, context); + return this.make(owner, thisArg, target, context); } } \ No newline at end of file From 7da7454ed4da0f91fb938e9e68733084c9a65c13 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 11:46:29 +0100 Subject: [PATCH 34/36] Improve description / note, for inherit() --- packages/contracts/src/support/meta/TargetRepository.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/support/meta/TargetRepository.ts b/packages/contracts/src/support/meta/TargetRepository.ts index 7456a36b..6bf4f05b 100644 --- a/packages/contracts/src/support/meta/TargetRepository.ts +++ b/packages/contracts/src/support/meta/TargetRepository.ts @@ -60,9 +60,8 @@ export default interface TargetRepository /** * 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 + * **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 From 23fba6887e8489c3dd988e217609a91b81e7586c Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 12:38:22 +0100 Subject: [PATCH 35/36] Refactor/Redesign targetMeta utils Added new TargetMetaRepository that handles the "targetMeta" logic. Furthermore, also added hasTargetMeta() util, and moved all target meta related components into the "target" sub-directory. --- packages/support/src/meta/index.ts | 2 - .../src/meta/target/TargetMetaRepository.ts | 430 ++++++++++++++++++ .../support/src/meta/target/getTargetMeta.ts | 26 ++ .../meta/target/getTargetMetaRepository.ts | 12 + .../support/src/meta/target/hasTargetMeta.ts | 15 + packages/support/src/meta/target/helpers.ts | 22 + packages/support/src/meta/target/index.ts | 12 + .../src/meta/target/inheritTargetMeta.ts | 43 ++ .../support/src/meta/target/targetMeta.ts | 47 ++ packages/support/src/meta/targetMeta.ts | 362 --------------- .../support/meta/inheritTargetMeta.test.js | 9 +- .../packages/support/meta/targetMeta.test.js | 17 +- 12 files changed, 626 insertions(+), 371 deletions(-) create mode 100644 packages/support/src/meta/target/TargetMetaRepository.ts create mode 100644 packages/support/src/meta/target/getTargetMeta.ts create mode 100644 packages/support/src/meta/target/getTargetMetaRepository.ts create mode 100644 packages/support/src/meta/target/hasTargetMeta.ts create mode 100644 packages/support/src/meta/target/helpers.ts create mode 100644 packages/support/src/meta/target/index.ts create mode 100644 packages/support/src/meta/target/inheritTargetMeta.ts create mode 100644 packages/support/src/meta/target/targetMeta.ts delete mode 100644 packages/support/src/meta/targetMeta.ts diff --git a/packages/support/src/meta/index.ts b/packages/support/src/meta/index.ts index fa613162..082583a1 100644 --- a/packages/support/src/meta/index.ts +++ b/packages/support/src/meta/index.ts @@ -12,7 +12,5 @@ export * from './hasMeta'; export * from './getAllMeta'; export * from './getMeta'; export * from './meta'; -export * from './targetMeta'; - export * from './exceptions'; export * from './target/index' \ 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 38fc6a88..00000000 --- a/packages/support/src/meta/targetMeta.ts +++ /dev/null @@ -1,362 +0,0 @@ -import type { - ClassDecorator, - ClassMethodDecorator -} from "@aedart/contracts"; -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 } from "./meta"; -import { getMeta } from "./getMeta"; - -/** - * 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 {ClassDecorator | ClassMethodDecorator} - * - * @throws {TypeError} When decorated element is not supported - */ -export function targetMeta( - key: Key | MetaCallback, - value?: unknown -): ClassDecorator | ClassMethodDecorator -{ - 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 - ); - }) as ClassDecorator | ClassMethodDecorator; -} - -/** - * 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); }); }); From 848a399582cd5501b6c90dafbcca5a050b7d151a Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 12:39:23 +0100 Subject: [PATCH 36/36] Change release notes --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3370233d..65e35a7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,13 +10,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * `hasMeta()` util, in `@aedart/support/meta`. -* `MetaRepository` 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`.