From 73ad0ff5f62accdc05a310922100d16e0c40e4c0 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 13 Feb 2024 20:27:45 +0100 Subject: [PATCH 001/424] Add contracts/support/concerns submodule --- aliases.js | 1 + packages/contracts/package.json | 5 +++++ packages/contracts/rollup.config.mjs | 1 + packages/contracts/src/support/concerns/index.ts | 6 ++++++ 4 files changed, 13 insertions(+) create mode 100644 packages/contracts/src/support/concerns/index.ts diff --git a/aliases.js b/aliases.js index 40dbdbed..9b1958d8 100644 --- a/aliases.js +++ b/aliases.js @@ -19,6 +19,7 @@ module.exports = { alias: { // contracts + '@aedart/contracts/support/concerns': path.resolve(__dirname, './packages/contracts/support/concerns'), '@aedart/contracts/support/meta': path.resolve(__dirname, './packages/contracts/support/meta'), '@aedart/contracts/support/mixins': path.resolve(__dirname, './packages/contracts/support/mixins'), '@aedart/contracts/support/reflections': path.resolve(__dirname, './packages/contracts/support/reflections'), diff --git a/packages/contracts/package.json b/packages/contracts/package.json index ba3d0e94..98d738e1 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -36,6 +36,11 @@ "import": "./dist/esm/support.js", "require": "./dist/cjs/support.cjs" }, + "./support/concerns": { + "types": "./dist/types/support/concerns.d.ts", + "import": "./dist/esm/support/concerns.js", + "require": "./dist/cjs/support/concerns.cjs" + }, "./support/meta": { "types": "./dist/types/support/meta.d.ts", "import": "./dist/esm/support/meta.js", diff --git a/packages/contracts/rollup.config.mjs b/packages/contracts/rollup.config.mjs index a584e216..bf68caf7 100644 --- a/packages/contracts/rollup.config.mjs +++ b/packages/contracts/rollup.config.mjs @@ -4,6 +4,7 @@ export default createConfig({ baseDir: new URL('.', import.meta.url), external: [ '@aedart/contracts/support', + '@aedart/contracts/support/concerns', '@aedart/contracts/support/meta', '@aedart/contracts/support/mixins', '@aedart/contracts/support/reflections', diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts new file mode 100644 index 00000000..101fd481 --- /dev/null +++ b/packages/contracts/src/support/concerns/index.ts @@ -0,0 +1,6 @@ +/** + * Support Concerns identifier + * + * @type {Symbol} + */ +export const SUPPORT_CONCERNS: unique symbol = Symbol('@aedart/contracts/support/concerns'); \ No newline at end of file From abfb916496a8b6461984917caf03fb186e1d561e Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 13 Feb 2024 20:31:52 +0100 Subject: [PATCH 002/424] Add support/concerns submodule --- packages/support/package.json | 5 +++++ packages/support/rollup.config.mjs | 1 + packages/support/src/concerns/index.ts | 2 ++ 3 files changed, 8 insertions(+) create mode 100644 packages/support/src/concerns/index.ts diff --git a/packages/support/package.json b/packages/support/package.json index 00923bff..f7e5b377 100644 --- a/packages/support/package.json +++ b/packages/support/package.json @@ -28,6 +28,11 @@ "import": "./dist/esm/support.js", "require": "./dist/cjs/support.cjs" }, + "./concerns": { + "types": "./dist/types/concerns.d.ts", + "import": "./dist/esm/concerns.js", + "require": "./dist/cjs/concerns.cjs" + }, "./meta": { "types": "./dist/types/meta.d.ts", "import": "./dist/esm/meta.js", diff --git a/packages/support/rollup.config.mjs b/packages/support/rollup.config.mjs index a584e216..bf68caf7 100644 --- a/packages/support/rollup.config.mjs +++ b/packages/support/rollup.config.mjs @@ -4,6 +4,7 @@ export default createConfig({ baseDir: new URL('.', import.meta.url), external: [ '@aedart/contracts/support', + '@aedart/contracts/support/concerns', '@aedart/contracts/support/meta', '@aedart/contracts/support/mixins', '@aedart/contracts/support/reflections', diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts new file mode 100644 index 00000000..24e3f534 --- /dev/null +++ b/packages/support/src/concerns/index.ts @@ -0,0 +1,2 @@ +// TODO: Remove this temp const... +export const TMP: string = 'tmp'; \ No newline at end of file From 6b679e061bc66039327349e54b81174ec6ed7e7e Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 13 Feb 2024 20:36:24 +0100 Subject: [PATCH 003/424] Add Concern interface (incomplete) --- packages/contracts/src/support/concerns/Concern.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/contracts/src/support/concerns/Concern.ts diff --git a/packages/contracts/src/support/concerns/Concern.ts b/packages/contracts/src/support/concerns/Concern.ts new file mode 100644 index 00000000..b3cd55e4 --- /dev/null +++ b/packages/contracts/src/support/concerns/Concern.ts @@ -0,0 +1,7 @@ +/** + * Concern + */ +export default interface Concern +{ + +} \ No newline at end of file From 104b6df401737934097c7cdd468cd4aa4e82764f Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 13 Feb 2024 20:36:32 +0100 Subject: [PATCH 004/424] Export Concern interface --- packages/contracts/src/support/concerns/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 101fd481..c1f1fc6b 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -1,6 +1,12 @@ +import Concern from "./Concern"; + /** * Support Concerns identifier * * @type {Symbol} */ -export const SUPPORT_CONCERNS: unique symbol = Symbol('@aedart/contracts/support/concerns'); \ No newline at end of file +export const SUPPORT_CONCERNS: unique symbol = Symbol('@aedart/contracts/support/concerns'); + +export { + type Concern +} \ No newline at end of file From f56ef9721c2c6c35b767dc4e5158a245c8bbacb4 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 13 Feb 2024 21:09:28 +0100 Subject: [PATCH 005/424] Add aliases type --- packages/contracts/src/support/concerns/types.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/contracts/src/support/concerns/types.ts diff --git a/packages/contracts/src/support/concerns/types.ts b/packages/contracts/src/support/concerns/types.ts new file mode 100644 index 00000000..e990bee6 --- /dev/null +++ b/packages/contracts/src/support/concerns/types.ts @@ -0,0 +1,8 @@ +import Concern from "./Concern"; + +/** + * A record that defines one or more aliases for a {@link Concern}'s properties or methods. + */ +export type Aliases = { + [P in keyof T]: PropertyKey +} \ No newline at end of file From 3ed5339c814da068b919c066fd2ca212cd6d882d Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 13 Feb 2024 21:10:20 +0100 Subject: [PATCH 006/424] Add Concern Configuration interface --- .../src/support/concerns/Configuration.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 packages/contracts/src/support/concerns/Configuration.ts diff --git a/packages/contracts/src/support/concerns/Configuration.ts b/packages/contracts/src/support/concerns/Configuration.ts new file mode 100644 index 00000000..906e6f65 --- /dev/null +++ b/packages/contracts/src/support/concerns/Configuration.ts @@ -0,0 +1,23 @@ +import Concern from "./Concern"; +import type { + Aliases +} from "./types"; + +/** + * Concern Configuration + * + * Defines the target Concern that must be injected into a target class, + * along with what aliases to be created. + */ +export default interface Configuration +{ + /** + * The target Concern Class this configuration is for + */ + concern: T; + + /** + * Aliases for Concern's properties or methods. + */ + aliases?: Aliases +} \ No newline at end of file From 2e275b55b0122d23df16d27a4d65f6b75f204daf Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 13 Feb 2024 21:10:35 +0100 Subject: [PATCH 007/424] Export types and Concern Configuration interface --- packages/contracts/src/support/concerns/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index c1f1fc6b..912c5e39 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -1,4 +1,5 @@ import Concern from "./Concern"; +import Configuration from "./Configuration"; /** * Support Concerns identifier @@ -8,5 +9,8 @@ import Concern from "./Concern"; export const SUPPORT_CONCERNS: unique symbol = Symbol('@aedart/contracts/support/concerns'); export { - type Concern -} \ No newline at end of file + type Concern, + type Configuration +} + +export * from './types'; \ No newline at end of file From a42a36302c99657d7acb46465dd20661f6c48d3f Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 10:10:15 +0100 Subject: [PATCH 008/424] Improve description --- packages/contracts/src/support/concerns/Configuration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/Configuration.ts b/packages/contracts/src/support/concerns/Configuration.ts index 906e6f65..484372fd 100644 --- a/packages/contracts/src/support/concerns/Configuration.ts +++ b/packages/contracts/src/support/concerns/Configuration.ts @@ -6,8 +6,8 @@ import type { /** * Concern Configuration * - * Defines the target Concern that must be injected into a target class, - * along with what aliases to be created. + * Defines the Concern class that must be injected into a target class, + * along with what aliases to be created in the target class. */ export default interface Configuration { From 96ec33b83155d18d0759b21bce0c0bf1992f280b Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 10:26:36 +0100 Subject: [PATCH 009/424] Add hidden symbol --- packages/contracts/src/support/concerns/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 912c5e39..e90d0da8 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -8,6 +8,14 @@ import Configuration from "./Configuration"; */ export const SUPPORT_CONCERNS: unique symbol = Symbol('@aedart/contracts/support/concerns'); +/** + * Symbol used by a {@link Concern} to define properties or methods that must be + * "hidden" and not allowed to be aliased into a target class. + * + * @type {Symbol} + */ +export const HIDDEN: unique symbol = Symbol('hidden'); + export { type Concern, type Configuration From bc45baf40c665b59bb2d0e3ccc778e10c015f4f3 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 10:32:51 +0100 Subject: [PATCH 010/424] Add constructor and get concern owner A concern should at the very minimum be able to obtain the target class instance that it was injected into. --- packages/contracts/src/support/concerns/Concern.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/contracts/src/support/concerns/Concern.ts b/packages/contracts/src/support/concerns/Concern.ts index b3cd55e4..9c1ae190 100644 --- a/packages/contracts/src/support/concerns/Concern.ts +++ b/packages/contracts/src/support/concerns/Concern.ts @@ -1,7 +1,21 @@ /** * Concern + * + * A component that can be injected into a target class (concern owner). */ export default interface Concern { + /** + * Creates a new concern instance + * + * @param {object} owner The target class instance this concern was injected into + */ + constructor(owner: object); + /** + * Returns the target class instance this concern was injected into + * + * @return {object} + */ + get concernOwner(): object; } \ No newline at end of file From 154b7c61e79e00d1c06c2f24f2e34161779825d0 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 10:36:49 +0100 Subject: [PATCH 011/424] Allow constructor to fail if concern does not support owner instance --- packages/contracts/src/support/concerns/Concern.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contracts/src/support/concerns/Concern.ts b/packages/contracts/src/support/concerns/Concern.ts index 9c1ae190..b0d1a945 100644 --- a/packages/contracts/src/support/concerns/Concern.ts +++ b/packages/contracts/src/support/concerns/Concern.ts @@ -9,6 +9,8 @@ export default interface Concern * Creates a new concern instance * * @param {object} owner The target class instance this concern was injected into + * + * @throws {TypeError} If this concern does not support given owner */ constructor(owner: object); From 7214d6beb13df32eb49f42002448212977994eed Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 11:01:27 +0100 Subject: [PATCH 012/424] Encourage constructor to throw error, if needed --- packages/contracts/src/support/concerns/Concern.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/support/concerns/Concern.ts b/packages/contracts/src/support/concerns/Concern.ts index b0d1a945..f5340e5b 100644 --- a/packages/contracts/src/support/concerns/Concern.ts +++ b/packages/contracts/src/support/concerns/Concern.ts @@ -1,7 +1,8 @@ /** * Concern - * - * A component that can be injected into a target class (concern owner). + * + * A "concern" is component that can be injected into a target class (concern owner). + * The concern itself is NOT responsible for performing the actual injection logic. */ export default interface Concern { @@ -10,7 +11,8 @@ export default interface Concern * * @param {object} owner The target class instance this concern was injected into * - * @throws {TypeError} If this concern does not support given owner + * @throws {Error} When concern is unable to preform initialisation, e.g. caused + * by the owner or other circumstances. */ constructor(owner: object); From 7a06ce25d2d8a5e7f926c04722655ad0008d512a Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 11:04:39 +0100 Subject: [PATCH 013/424] Improve JSDoc --- packages/contracts/src/support/concerns/Concern.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contracts/src/support/concerns/Concern.ts b/packages/contracts/src/support/concerns/Concern.ts index f5340e5b..c0a2003d 100644 --- a/packages/contracts/src/support/concerns/Concern.ts +++ b/packages/contracts/src/support/concerns/Concern.ts @@ -3,6 +3,8 @@ * * A "concern" is component that can be injected into a target class (concern owner). * The concern itself is NOT responsible for performing the actual injection logic. + * + * @interface */ export default interface Concern { From 404cf5700d3d2d5c3dcdd01a902d43ace8555530 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 11:06:12 +0100 Subject: [PATCH 014/424] Fix typo --- packages/contracts/src/support/concerns/Concern.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/Concern.ts b/packages/contracts/src/support/concerns/Concern.ts index c0a2003d..7936bebd 100644 --- a/packages/contracts/src/support/concerns/Concern.ts +++ b/packages/contracts/src/support/concerns/Concern.ts @@ -11,7 +11,7 @@ export default interface Concern /** * Creates a new concern instance * - * @param {object} owner The target class instance this concern was injected into + * @param {object} owner The target class instance this concern is injected into * * @throws {Error} When concern is unable to preform initialisation, e.g. caused * by the owner or other circumstances. @@ -19,7 +19,7 @@ export default interface Concern constructor(owner: object); /** - * Returns the target class instance this concern was injected into + * Returns the target class instance this concern is injected into * * @return {object} */ From 562104405218374a7a949d3ecbc9e443bc63409f Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 11:29:50 +0100 Subject: [PATCH 015/424] Remove constructor signature from interface Ups, eslint / TypeScript does not allow constructors in interfaces. --- packages/contracts/src/support/concerns/Concern.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/contracts/src/support/concerns/Concern.ts b/packages/contracts/src/support/concerns/Concern.ts index 7936bebd..99c9a907 100644 --- a/packages/contracts/src/support/concerns/Concern.ts +++ b/packages/contracts/src/support/concerns/Concern.ts @@ -8,16 +8,6 @@ */ export default interface Concern { - /** - * Creates a new concern instance - * - * @param {object} owner The target class instance this concern is injected into - * - * @throws {Error} When concern is unable to preform initialisation, e.g. caused - * by the owner or other circumstances. - */ - constructor(owner: object); - /** * Returns the target class instance this concern is injected into * From 6f4a11fd25713ed4798bb4b5720abde1980b72db Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 11:32:08 +0100 Subject: [PATCH 016/424] Add an "always hidden" array List defines what properties and methods should never be aliased into a target class. --- .../contracts/src/support/concerns/index.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index e90d0da8..f39a39a4 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -16,6 +16,25 @@ export const SUPPORT_CONCERNS: unique symbol = Symbol('@aedart/contracts/support */ export const HIDDEN: unique symbol = Symbol('hidden'); +/** + * List of properties and methods that must always remain "hidden" and + * NEVER be aliased into a target class. + * + * @type {ReadonlyArray} + */ +export const ALWAYS_HIDDEN: ReadonlyArray = [ + // It is NOT possible, nor advised to attempt to alias a Concern's + // constructor into a target class. + 'constructor', + + // The concernOwner property (getter) will not work either... + 'concernOwner', + + // Lastly, if the Concern defines any hidden properties or methods, + // then such a method will not do any good in a target class. + HIDDEN +]; + export { type Concern, type Configuration From eb90db4329c0cc3d911791d4105e6d2e601aa9a2 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 11:45:16 +0100 Subject: [PATCH 017/424] Add abstract concern class --- packages/support/src/concerns/BaseConcern.ts | 60 ++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 packages/support/src/concerns/BaseConcern.ts diff --git a/packages/support/src/concerns/BaseConcern.ts b/packages/support/src/concerns/BaseConcern.ts new file mode 100644 index 00000000..d6505be0 --- /dev/null +++ b/packages/support/src/concerns/BaseConcern.ts @@ -0,0 +1,60 @@ +import type { Concern } from "@aedart/contracts/support/concerns"; +import { HIDDEN, ALWAYS_HIDDEN } from "@aedart/contracts/support/concerns"; + +/** + * Base Concern + * + * Abstraction for a {@link Concern} component. + * + * @implements {Concern} + * @abstract + */ +export default abstract class BaseConcern implements Concern +{ + /** + * The target class instance this concern is injected into + * + * @private + * @type {object} + */ + readonly #concernOwner: object; + + /** + * Creates a new concern instance + * + * @param {object} owner The target class instance this concern is injected into + * + * @throws {Error} When concern is unable to preform initialisation, e.g. caused + * by the owner or other circumstances. + */ + constructor(owner: object) + { + if (new.target === BaseConcern) { + throw new Error('Unable to instantiate new instance of abstract class BaseConcern'); + } + + this.#concernOwner = owner; + } + + /** + * @inheritdoc + */ + get concernOwner(): object + { + return this.#concernOwner; + } + + /** + * Returns a list of properties and methods that MUST NOT be aliased into the target class. + * + * **Warning**: _Regardless of what properties and methods this method may return, + * an "injector" component that applies this concern MUST ensure that the {@link ALWAYS_HIDDEN} + * defined properties and methods are **NEVER** aliased into a target class._ + * + * @return {PropertyKey[]} + */ + static [HIDDEN](): PropertyKey[] + { + return ALWAYS_HIDDEN as PropertyKey[]; + } +} \ No newline at end of file From eeb82d6e4b672be8aa29f9d3271fcb7b44dd3c5a Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 11:45:28 +0100 Subject: [PATCH 018/424] Export abstract concern --- packages/support/src/concerns/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 24e3f534..206b026b 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -1,2 +1,5 @@ -// TODO: Remove this temp const... -export const TMP: string = 'tmp'; \ No newline at end of file +import BaseConcern from "./BaseConcern"; + +export { + BaseConcern +}; \ No newline at end of file From eea425849827bc847702ca68879525f4b36bb3ed Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 11:46:57 +0100 Subject: [PATCH 019/424] Rename to Abstract Concern --- .../src/concerns/{BaseConcern.ts => AbstractConcern.ts} | 9 ++++----- packages/support/src/concerns/index.ts | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) rename packages/support/src/concerns/{BaseConcern.ts => AbstractConcern.ts} (89%) diff --git a/packages/support/src/concerns/BaseConcern.ts b/packages/support/src/concerns/AbstractConcern.ts similarity index 89% rename from packages/support/src/concerns/BaseConcern.ts rename to packages/support/src/concerns/AbstractConcern.ts index d6505be0..307de2e1 100644 --- a/packages/support/src/concerns/BaseConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -2,14 +2,13 @@ import type { Concern } from "@aedart/contracts/support/concerns"; import { HIDDEN, ALWAYS_HIDDEN } from "@aedart/contracts/support/concerns"; /** - * Base Concern - * - * Abstraction for a {@link Concern} component. + * Abstract Concern * + * @see {Concern} * @implements {Concern} * @abstract */ -export default abstract class BaseConcern implements Concern +export default abstract class AbstractConcern implements Concern { /** * The target class instance this concern is injected into @@ -29,7 +28,7 @@ export default abstract class BaseConcern implements Concern */ constructor(owner: object) { - if (new.target === BaseConcern) { + if (new.target === AbstractConcern) { throw new Error('Unable to instantiate new instance of abstract class BaseConcern'); } diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 206b026b..f60c1b4d 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -1,5 +1,5 @@ -import BaseConcern from "./BaseConcern"; +import AbstractConcern from "./AbstractConcern"; export { - BaseConcern + AbstractConcern }; \ No newline at end of file From 31a578688ca674c4e423c66d7ef70f794032bc09 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 11:47:32 +0100 Subject: [PATCH 020/424] Fix error message --- packages/support/src/concerns/AbstractConcern.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index 307de2e1..6af14ef2 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -29,7 +29,7 @@ export default abstract class AbstractConcern implements Concern constructor(owner: object) { if (new.target === AbstractConcern) { - throw new Error('Unable to instantiate new instance of abstract class BaseConcern'); + throw new Error('Unable to instantiate new instance of abstract class'); } this.#concernOwner = owner; From 70968dc6541fe2f5e7ccceca0b5267fb02cf8bb5 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 11:52:42 +0100 Subject: [PATCH 021/424] Change constructor visibility to protected --- packages/support/src/concerns/AbstractConcern.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index 6af14ef2..1b52c9f8 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -21,12 +21,13 @@ export default abstract class AbstractConcern implements Concern /** * Creates a new concern instance * + * @protected * @param {object} owner The target class instance this concern is injected into * * @throws {Error} When concern is unable to preform initialisation, e.g. caused * by the owner or other circumstances. */ - constructor(owner: object) + protected constructor(owner: object) { if (new.target === AbstractConcern) { throw new Error('Unable to instantiate new instance of abstract class'); From e253f6cbca9c67e9988fddfbb88fba7c83f8a9f1 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 12:46:06 +0100 Subject: [PATCH 022/424] Improve description of aliases object --- packages/contracts/src/support/concerns/types.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/contracts/src/support/concerns/types.ts b/packages/contracts/src/support/concerns/types.ts index e990bee6..1181078f 100644 --- a/packages/contracts/src/support/concerns/types.ts +++ b/packages/contracts/src/support/concerns/types.ts @@ -2,6 +2,10 @@ import Concern from "./Concern"; /** * A record that defines one or more aliases for a {@link Concern}'s properties or methods. + * + * In this context an "alias" means a property or method name that is added onto a target + * class' prototype and acts as a proxy to the original property or method inside the + * concern class. */ export type Aliases = { [P in keyof T]: PropertyKey From 92a0d69ebb3056efd797966f8653c6a70e69910c Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 13:25:05 +0100 Subject: [PATCH 023/424] Change configuration, improve types and add "hidden" The "hidden" is an attempt to overwrite whatever a Concern class defines and allow developers further flexibility when injecting into a target class. Also, the "allowAliases" flag will ensure that no unwanted proxy properties or methods are created in the target class. --- .../src/support/concerns/Configuration.ts | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/contracts/src/support/concerns/Configuration.ts b/packages/contracts/src/support/concerns/Configuration.ts index 484372fd..c9fd8458 100644 --- a/packages/contracts/src/support/concerns/Configuration.ts +++ b/packages/contracts/src/support/concerns/Configuration.ts @@ -1,23 +1,62 @@ +import type { Constructor } from "@aedart/contracts"; import Concern from "./Concern"; +import { HIDDEN, ALWAYS_HIDDEN } from "./index"; import type { Aliases } from "./types"; /** - * Concern Configuration + * Concern Injection Configuration * * Defines the Concern class that must be injected into a target class, - * along with what aliases to be created in the target class. + * along with other aspects of the injection, such as what aliases to use + * and what properties or methods should not be aliased. + * + * @see {Concern} + * @see {Aliases} + * @see {HIDDEN} */ export default interface Configuration { /** - * The target Concern Class this configuration is for + * The Concern Class that must be injected into a target class + * + * @type {Constructor} */ - concern: T; + concern: Constructor; /** * Aliases for Concern's properties or methods. + * + * **Note**: _An "injector" must always default to the same property and + * method names as those defined in the Concern Class, if a given property or + * method is not specified here._ + * + * @type {Aliases|undefined} + */ + aliases?: Aliases; + + /** + * Properties and methods that MUST NOT be aliased into a target class. + * + * **Note**: _Defaults to list provided by the Concern Class' {@link HIDDEN}, + * if not specified here._ + * + * **Note**: _Properties and methods that are defined in {@link ALWAYS_HIDDEN} + * will always be hidden, regardless of those defined here._ + * + * @type {PropertyKey[]|undefined} + */ + hidden?: PropertyKey[]; + + /** + * Flag that indicates whether an "injector" is allowed to create + * "aliases" (proxy) properties and methods into a target class's prototype. + * + * If set to `false`, then {@link aliases} and {@link hidden} settings + * are ignored. + * + * @type {boolean} */ - aliases?: Aliases + allowAliases: boolean; } \ No newline at end of file From beb6d861f9e854eb36bc079146ab2f0b572349e2 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 13:29:39 +0100 Subject: [PATCH 024/424] Revert protected visibility for constructor Not sure why my IDE wanted this... A concrete Concern class shouldn't be forced to implement a constructor! --- packages/support/src/concerns/AbstractConcern.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index 1b52c9f8..2f78447c 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -21,13 +21,12 @@ export default abstract class AbstractConcern implements Concern /** * Creates a new concern instance * - * @protected * @param {object} owner The target class instance this concern is injected into * * @throws {Error} When concern is unable to preform initialisation, e.g. caused * by the owner or other circumstances. */ - protected constructor(owner: object) + public constructor(owner: object) { if (new.target === AbstractConcern) { throw new Error('Unable to instantiate new instance of abstract class'); From 72c655a453a40f235eb9b60463ce2d33a2dd6fa6 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 13:33:50 +0100 Subject: [PATCH 025/424] Fix unused vars --- packages/contracts/src/support/concerns/Configuration.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/contracts/src/support/concerns/Configuration.ts b/packages/contracts/src/support/concerns/Configuration.ts index c9fd8458..04eb6dc4 100644 --- a/packages/contracts/src/support/concerns/Configuration.ts +++ b/packages/contracts/src/support/concerns/Configuration.ts @@ -1,6 +1,5 @@ import type { Constructor } from "@aedart/contracts"; import Concern from "./Concern"; -import { HIDDEN, ALWAYS_HIDDEN } from "./index"; import type { Aliases } from "./types"; @@ -14,7 +13,6 @@ import type { * * @see {Concern} * @see {Aliases} - * @see {HIDDEN} */ export default interface Configuration { @@ -39,10 +37,10 @@ export default interface Configuration /** * Properties and methods that MUST NOT be aliased into a target class. * - * **Note**: _Defaults to list provided by the Concern Class' {@link HIDDEN}, + * **Note**: _Defaults to list provided by the Concern Class' [HIDDEN]{@link import('@aedart/contracts/support/concerns').HIDDEN}, * if not specified here._ * - * **Note**: _Properties and methods that are defined in {@link ALWAYS_HIDDEN} + * **Note**: _Properties and methods that are defined in [ALWAYS_HIDDEN]{@link import('@aedart/contracts/support/concerns').ALWAYS_HIDDEN} * will always be hidden, regardless of those defined here._ * * @type {PropertyKey[]|undefined} From cfc02f2770172b41a9e59c137979a4817d5bd37a Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 13:41:02 +0100 Subject: [PATCH 026/424] Improve description of HIDDEN symbol --- packages/contracts/src/support/concerns/index.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index f39a39a4..4f2cf3e5 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -12,6 +12,21 @@ export const SUPPORT_CONCERNS: unique symbol = Symbol('@aedart/contracts/support * Symbol used by a {@link Concern} to define properties or methods that must be * "hidden" and not allowed to be aliased into a target class. * + * **Note**: _Symbol MUST be used to as name for a "static" method in the desired Concern class._ + * + * **Example**: + * ```ts + * class MyConcern implements Concern + * { + * static [HIDDEN](): PropertyKey[] + * { + * // ...not shown... + * } + * + * // ...remaining not shown... + * } + * ``` + * * @type {Symbol} */ export const HIDDEN: unique symbol = Symbol('hidden'); From 114c04dd738a9abc54267674ff2744d71600b4da Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 13:46:20 +0100 Subject: [PATCH 027/424] Improve error message --- packages/support/src/concerns/AbstractConcern.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index 2f78447c..5bc2ac4c 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -29,7 +29,7 @@ export default abstract class AbstractConcern implements Concern public constructor(owner: object) { if (new.target === AbstractConcern) { - throw new Error('Unable to instantiate new instance of abstract class'); + throw new Error('Unable to make a new instance of abstract class'); } this.#concernOwner = owner; From a51aae3e539eb39772ca66109715540bd6b1d04b Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 14:07:31 +0100 Subject: [PATCH 028/424] Improve method description --- packages/support/src/concerns/AbstractConcern.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index 5bc2ac4c..48a88f05 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -47,7 +47,7 @@ export default abstract class AbstractConcern implements Concern * Returns a list of properties and methods that MUST NOT be aliased into the target class. * * **Warning**: _Regardless of what properties and methods this method may return, - * an "injector" component that applies this concern MUST ensure that the {@link ALWAYS_HIDDEN} + * an "injector" that injects this concern MUST ensure that the {@link ALWAYS_HIDDEN} * defined properties and methods are **NEVER** aliased into a target class._ * * @return {PropertyKey[]} From 51b733c98c110747a18509a59db0a70ead321e0f Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 15:05:08 +0100 Subject: [PATCH 029/424] Improve Aliases type definition --- packages/contracts/src/support/concerns/types.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/contracts/src/support/concerns/types.ts b/packages/contracts/src/support/concerns/types.ts index 1181078f..d0dcaf2b 100644 --- a/packages/contracts/src/support/concerns/types.ts +++ b/packages/contracts/src/support/concerns/types.ts @@ -1,12 +1,15 @@ import Concern from "./Concern"; +/** + * An alias for a property or method in a {@link Concern} class + */ +export type Alias = PropertyKey; + /** * A record that defines one or more aliases for a {@link Concern}'s properties or methods. * - * In this context an "alias" means a property or method name that is added onto a target + * In this context an "alias" means a property or method that is added onto a target * class' prototype and acts as a proxy to the original property or method inside the - * concern class. + * concern class instance. */ -export type Aliases = { - [P in keyof T]: PropertyKey -} \ No newline at end of file +export type Aliases = Record; \ No newline at end of file From 34381a03ad52f60db31f2c44992f9f2af31d3fbc Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 15:24:46 +0100 Subject: [PATCH 030/424] Add Injector interface (incomplete) --- .../src/support/concerns/Injector.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 packages/contracts/src/support/concerns/Injector.ts diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts new file mode 100644 index 00000000..93c57bd6 --- /dev/null +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -0,0 +1,70 @@ +import { Constructor, ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import Concern from "./Concern"; +import Configuration from "./Configuration"; + +/** + * Concerns Injector + * + * Able to inject concerns into a target class and create alias (proxy) properties + * and methods to the provided concerns' properties and methods, in the target class. + * + * @see {Concern} + */ +export default interface Injector +{ + /** + * Returns the target class that concerns must be injected into + * + * @return {ConstructorOrAbstractConstructor} + */ + get target(): T; + + /** + * Inject one or more concerns into the target class and return the modified target + * + * TODO: Details description what this method must and MUST NOT do... + * TODO: E.g. define a "CONCERNS" property / map / method in target class' prototype (with inherited concerns from parent classes?) + * TODO: E.g. must prevent duplicate concern injection target (full inheritance chain) + * TODO: E.g. must prevent name conflicts in aliases + * + * @template C = {@link Concern} + * + * @param {...Constructor|Configuration} concerns + * + * @return {ConstructorOrAbstractConstructor} + * + * @throws {Error} If unable to inject concerns into target class + */ + inject(...concerns: Constructor|Configuration): T; + + /** + * Create aliases (a proxies) in target class, to the properties or methods in given concern + * + * **Note**: _Method creates each alias using the {@link createAlias} method!_ + * + * @param {Configuration} config The concern injection configuration + * + * @throws {Error} If unable to obtain keys from source concern or failure occurs when defining + * proxy properties or methods in target class + */ + createAliases(config: Configuration): void; + + /** + * Create an alias (a proxy) in target class, to a property or method in given concern + * + * **Note**: _Method will do nothing if a property or method exists in the target, with the same name + * as the given alias!_ + * + * @param {PropertyKey} key Name of the property or method in the source concern class to create alias for + * @param {PropertyKey} alias Alias for the key to create in the target class (the proxy property or method) + * @param {Constructor} source The concern to that the alias property or method must proxy to + * + * @return {boolean} True if a proxy property or method was created in target class. + * False if not, e.g. property or method with same name or symbol as the alias + * already exists in target. + * + * @throws {Error} If unable to obtain key from source concern or failure occurs when defining + * proxy property or method in target + */ + createAlias(key: PropertyKey, alias: PropertyKey, source: Constructor): boolean; +} \ No newline at end of file From ea0288e6531ed158a32dbbe95210229bd767ea6b Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 15:24:59 +0100 Subject: [PATCH 031/424] Export Injector interface --- packages/contracts/src/support/concerns/index.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 4f2cf3e5..4eeaa4c6 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -1,6 +1,3 @@ -import Concern from "./Concern"; -import Configuration from "./Configuration"; - /** * Support Concerns identifier * @@ -50,9 +47,13 @@ export const ALWAYS_HIDDEN: ReadonlyArray = [ HIDDEN ]; +import Concern from "./Concern"; +import Configuration from "./Configuration"; +import Injector from "./Injector"; export { type Concern, - type Configuration + type Configuration, + type Injector } export * from './types'; \ No newline at end of file From 7ef3bb5247583136e863fae1fe14d75e4c3a9e9d Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 15:59:48 +0100 Subject: [PATCH 032/424] Add Concerns Container interface (incomplete) --- .../src/support/concerns/Container.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 packages/contracts/src/support/concerns/Container.ts diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts new file mode 100644 index 00000000..5f34622f --- /dev/null +++ b/packages/contracts/src/support/concerns/Container.ts @@ -0,0 +1,31 @@ +import type { Constructor } from "@aedart/contracts"; +import type { Concern } from "./Concern"; + +/** + * Concerns Container + */ +export default interface Container +{ + /** + * Determine if a concern class exists in this map + * + * @template T extends {@link Concern} + * + * @param {Constructor} concern + * + * @return {boolean} + */ + has(concern: Constructor): boolean; + + // TODO: + get(concern: Constructor): Concern|null; + + // TODO: + hasBooted(concern: Constructor): boolean + + // TODO: + boot(concern: Constructor): void + + // TODO: + all(): Constructor[]; +} \ No newline at end of file From 0fb6899915578dcdeb7f7791915f95e32204ff6f Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 16:08:16 +0100 Subject: [PATCH 033/424] Add Concerns Owner interface Defines the read only "concerns" property that a target class instance must define to have concerns injected. --- packages/contracts/src/support/concerns/Owner.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 packages/contracts/src/support/concerns/Owner.ts diff --git a/packages/contracts/src/support/concerns/Owner.ts b/packages/contracts/src/support/concerns/Owner.ts new file mode 100644 index 00000000..1775f753 --- /dev/null +++ b/packages/contracts/src/support/concerns/Owner.ts @@ -0,0 +1,15 @@ +import Container from "./Container"; +import { CONCERNS } from "./index"; + +/** + * Concerns Owner + */ +export default interface Owner +{ + /** + * Get the concerns container for this class + * + * @type {Container} + */ + readonly [CONCERNS](): Container; +} \ No newline at end of file From c554084aedd4b73452febac18ae6096fc91fbefa Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 16:09:43 +0100 Subject: [PATCH 034/424] Define concerns symbol and export Container and Owner interfaces --- packages/contracts/src/support/concerns/index.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 4eeaa4c6..f9605b5e 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -47,13 +47,27 @@ export const ALWAYS_HIDDEN: ReadonlyArray = [ HIDDEN ]; +/** + * Symbol used to define a "concerns container" property inside a target class + * + * @see {Owner} + * @see {Container} + * + * @type {Symbol} + */ +export const CONCERNS: unique symbol = Symbol('concerns'); + import Concern from "./Concern"; import Configuration from "./Configuration"; +import Container from "./Container"; import Injector from "./Injector"; +import Owner from "./Owner"; export { type Concern, type Configuration, - type Injector + type Container, + type Injector, + type Owner } export * from './types'; \ No newline at end of file From 9708aa6df14bf257a207090c9e57870b4c11fb89 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 16:12:37 +0100 Subject: [PATCH 035/424] Fix import --- packages/contracts/src/support/concerns/Container.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index 5f34622f..7bc22cfc 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -1,5 +1,5 @@ import type { Constructor } from "@aedart/contracts"; -import type { Concern } from "./Concern"; +import Concern from "./Concern"; /** * Concerns Container From fab4234341646948f474ee322c857f5f605284e6 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 16:17:21 +0100 Subject: [PATCH 036/424] Fix concerns property definition --- packages/contracts/src/support/concerns/Owner.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/Owner.ts b/packages/contracts/src/support/concerns/Owner.ts index 1775f753..39559019 100644 --- a/packages/contracts/src/support/concerns/Owner.ts +++ b/packages/contracts/src/support/concerns/Owner.ts @@ -7,9 +7,11 @@ import { CONCERNS } from "./index"; export default interface Owner { /** - * Get the concerns container for this class + * The concerns container for this class + * + * @readonly * * @type {Container} */ - readonly [CONCERNS](): Container; + readonly [CONCERNS]: Container; } \ No newline at end of file From a2b4efa7ffa43f3a725bc3f0d97b22dc8f7f44c7 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 16:21:06 +0100 Subject: [PATCH 037/424] Improve container with additional util methods --- .../src/support/concerns/Container.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index 7bc22cfc..3a44032b 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -7,7 +7,16 @@ import Concern from "./Concern"; export default interface Container { /** - * Determine if a concern class exists in this map + * The amount of concerns in this container + * + * @readonly + * + * @type {number} + */ + readonly size: number; + + /** + * Determine if a concern class exists in this container * * @template T extends {@link Concern} * @@ -28,4 +37,18 @@ export default interface Container // TODO: all(): Constructor[]; + + /** + * Determine if this container is empty + * + * @return {boolean} + */ + isEmpty(): boolean; + + /** + * Opposite of {@link isEmpty} + * + * @return {boolean} + */ + isNotEmpty(): boolean; } \ No newline at end of file From e959749cee2fedcdcbea9955b82f5784fce25cb8 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 14 Feb 2024 16:25:41 +0100 Subject: [PATCH 038/424] Ensure that a container knows the owner class This will be needed for the boot method, but it does add a strange dependency... --- packages/contracts/src/support/concerns/Container.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index 3a44032b..51cb2635 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -1,5 +1,6 @@ import type { Constructor } from "@aedart/contracts"; import Concern from "./Concern"; +import Owner from "./Owner"; /** * Concerns Container @@ -14,6 +15,13 @@ export default interface Container * @type {number} */ readonly size: number; + + /** + * Get the concerns container owner + * + * @return {Owner} + */ + get owner(): Owner; /** * Determine if a concern class exists in this container @@ -33,7 +41,7 @@ export default interface Container hasBooted(concern: Constructor): boolean // TODO: - boot(concern: Constructor): void + boot(concern: Constructor): T // TODO: all(): Constructor[]; From b67e945b305ff83c7577ac786310dd40186c7ff4 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 10:11:59 +0100 Subject: [PATCH 039/424] Redesign API for Injector --- .../src/support/concerns/Injector.ts | 80 +++++++++++++------ 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index 93c57bd6..9b634b3f 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -1,6 +1,7 @@ import { Constructor, ConstructorOrAbstractConstructor } from "@aedart/contracts"; import Concern from "./Concern"; import Configuration from "./Configuration"; +import Container from "./Container"; /** * Concerns Injector @@ -10,51 +11,73 @@ import Configuration from "./Configuration"; * * @see {Concern} */ -export default interface Injector +export default interface Injector { /** - * Returns the target class that concerns must be injected into + * Inject one or more concerns into the target class and return the modified target. * - * @return {ConstructorOrAbstractConstructor} - */ - get target(): T; - - /** - * Inject one or more concerns into the target class and return the modified target + * **Note**: _This method performs the following (in given order):_ + * + * _**A**: Defines a new concerns {@link Container} in target class' prototype, via {@link defineContainer}._ * - * TODO: Details description what this method must and MUST NOT do... - * TODO: E.g. define a "CONCERNS" property / map / method in target class' prototype (with inherited concerns from parent classes?) - * TODO: E.g. must prevent duplicate concern injection target (full inheritance chain) - * TODO: E.g. must prevent name conflicts in aliases + * _**B**: Defines aliases (proxy properties and methods) in target class' prototype, via {@link defineAliases}._ * + * @template T extends {@link ConstructorOrAbstractConstructor} = object * @template C = {@link Concern} * + * @param {T} target The target class concerns must be injected into * @param {...Constructor|Configuration} concerns * - * @return {ConstructorOrAbstractConstructor} + * @return {T} Given target class with concern classes injected into its prototype * - * @throws {Error} If unable to inject concerns into target class + * @throws {TypeError|Error} */ - inject(...concerns: Constructor|Configuration): T; + inject< + T extends ConstructorOrAbstractConstructor = object, + C = Concern + >(target: T, ...concerns: Constructor|Configuration): T; + + /** + * Defines a concerns {@link Container} in target class' prototype using + * [CONCERNS]{@link import('@aedart/contracts/support/concerns').CONCERNS} as its property key. + * + * **Note**: _If target class has a parent that already has a container defined, then the + * concern classes it contains will be merged with those provided as argument for this method, + * and populated into the new container._ + * + * @param {object} target The target class in which a concerns container must be defined + * @param {Constructor[]} concerns The concern classes to populate the container with. + * + * @return {Container} + * + * @throws {TypeError} If duplicate concern classes are provided, or if provided concern classes are already + * defined in target class' parent. + * @throws {Error} If unable to define concerns container in target class. + */ + defineContainer(target: object, concerns: Constructor[]): Container; /** - * Create aliases (a proxies) in target class, to the properties or methods in given concern + * Create aliases (proxy properties and methods) in target class' prototype, to the properties + * or methods in given concerns. * - * **Note**: _Method creates each alias using the {@link createAlias} method!_ + * **Note**: _Method creates each alias using the {@link defineAlias} method!_ * - * @param {Configuration} config The concern injection configuration + * @param {object} target The target class aliases must be created in + * @param {Configuration[]} configurations List of concern injection configurations * + * @throws {TypeError} In case of conflicting aliases. * @throws {Error} If unable to obtain keys from source concern or failure occurs when defining - * proxy properties or methods in target class + * proxy properties or methods in target class' prototype. */ - createAliases(config: Configuration): void; + defineAliases(target: object, configurations: Configuration[]): void; /** - * Create an alias (a proxy) in target class, to a property or method in given concern - * - * **Note**: _Method will do nothing if a property or method exists in the target, with the same name - * as the given alias!_ + * Create an alias (a proxy) in target class' prototype, to a property or method in given concern. * + * **Note**: _Method will do nothing if a property or method already exists in the target, with the same + * name as the given alias!_ + * + * @param {object} target The target class the alias must be created in * @param {PropertyKey} key Name of the property or method in the source concern class to create alias for * @param {PropertyKey} alias Alias for the key to create in the target class (the proxy property or method) * @param {Constructor} source The concern to that the alias property or method must proxy to @@ -64,7 +87,12 @@ export default interface Injector): boolean; + defineAlias( + target: object, + key: PropertyKey, + alias: PropertyKey, + source: Constructor + ): boolean; } \ No newline at end of file From 39475d672989a9a7d0b4a23c3c44f5fd4f937b9a Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 10:12:23 +0100 Subject: [PATCH 040/424] Change always hidden, add CONCERNS symbol --- .../contracts/src/support/concerns/index.ts | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index f9605b5e..7c415f17 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -28,13 +28,27 @@ export const SUPPORT_CONCERNS: unique symbol = Symbol('@aedart/contracts/support */ export const HIDDEN: unique symbol = Symbol('hidden'); +/** + * Symbol used to define a "concerns container" property inside a target class' prototype + * + * @see {Owner} + * @see {Container} + * + * @type {Symbol} + */ +export const CONCERNS: unique symbol = Symbol('concerns'); + /** * List of properties and methods that must always remain "hidden" and - * NEVER be aliased into a target class. + * **NEVER** be aliased into a target class' prototype. * * @type {ReadonlyArray} */ export const ALWAYS_HIDDEN: ReadonlyArray = [ + // ----------------------------------------------------------------- // + // Defined by Concern: + // ----------------------------------------------------------------- // + // It is NOT possible, nor advised to attempt to alias a Concern's // constructor into a target class. 'constructor', @@ -42,20 +56,18 @@ export const ALWAYS_HIDDEN: ReadonlyArray = [ // The concernOwner property (getter) will not work either... 'concernOwner', - // Lastly, if the Concern defines any hidden properties or methods, + // If the Concern defines any hidden properties or methods, // then such a method will not do any good in a target class. - HIDDEN -]; + HIDDEN, -/** - * Symbol used to define a "concerns container" property inside a target class - * - * @see {Owner} - * @see {Container} - * - * @type {Symbol} - */ -export const CONCERNS: unique symbol = Symbol('concerns'); + // ----------------------------------------------------------------- // + // Other properties and methods: + // ----------------------------------------------------------------- // + + // In case that a concern class uses other concerns, prevent them + // from being aliased. + CONCERNS, +]; import Concern from "./Concern"; import Configuration from "./Configuration"; From 92e8c5cbac137b4b12d21554a1032dde74d3ba86 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 11:15:55 +0100 Subject: [PATCH 041/424] Change API for Container, add bootAll and missing JSDoc Also improved some of the types --- .../src/support/concerns/Container.ts | 71 +++++++++++++++---- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index 51cb2635..6efdfed7 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -24,28 +24,62 @@ export default interface Container get owner(): Owner; /** - * Determine if a concern class exists in this container + * Determine if concern class is registered in this container + * + * @param {Constructor} concern * - * @template T extends {@link Concern} + * @return {boolean} + */ + has(concern: Constructor): boolean; + + /** + * Retrieve concern instance for given concern class + * + * **Note**: _If concern class is registered in this container, but not yet + * booted, then this method will boot it via the {@link boot} method, and return + * the resulting instance._ * + * @template T extends {@link Concern} + * * @param {Constructor} concern + * + * @return {Concern|null} Concern instance or `null` if provided concern class + * is not registered in this container. + * + * @throws {Error} + */ + get(concern: Constructor): T|null; + + /** + * Determine if concern class has been booted + * + * @param {Constructor} concern * * @return {boolean} */ - has(concern: Constructor): boolean; - - // TODO: - get(concern: Constructor): Concern|null; - - // TODO: - hasBooted(concern: Constructor): boolean - - // TODO: - boot(concern: Constructor): T - - // TODO: - all(): Constructor[]; + hasBooted(concern: Constructor): boolean + + /** + * Boot concern class + * + * @template T extends {@link Concern} + * + * @param {Constructor} concern + * + * @return {Concern} New concern instance + * + * @throws {Error} If provided concern class has already been booted, or + * if not registered in this container. + */ + boot(concern: Constructor): T; + /** + * Boots all registered concern classes + * + * @throws {Error} + */ + bootAll(): void; + /** * Determine if this container is empty * @@ -59,4 +93,11 @@ export default interface Container * @return {boolean} */ isNotEmpty(): boolean; + + /** + * Returns all concern classes + * + * @return {Constructor[]} + */ + all(): Constructor[]; } \ No newline at end of file From bdff7dd32248584986ed6a11ce476636b9f9785d Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 11:22:14 +0100 Subject: [PATCH 042/424] Improve internal comments for ALWAYS_HIDDEN list --- packages/contracts/src/support/concerns/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 7c415f17..8ec393aa 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -46,7 +46,7 @@ export const CONCERNS: unique symbol = Symbol('concerns'); */ export const ALWAYS_HIDDEN: ReadonlyArray = [ // ----------------------------------------------------------------- // - // Defined by Concern: + // Defined by Concern interface / Abstract Concern: // ----------------------------------------------------------------- // // It is NOT possible, nor advised to attempt to alias a Concern's From afb32c5c88fe5389a6683ec971dfb00417f0d51d Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 12:23:37 +0100 Subject: [PATCH 043/424] Prohibit prototype from being aliased --- packages/contracts/src/support/concerns/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 8ec393aa..26124f6d 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -53,7 +53,7 @@ export const ALWAYS_HIDDEN: ReadonlyArray = [ // constructor into a target class. 'constructor', - // The concernOwner property (getter) will not work either... + // The concernOwner property (getter) shouldn't be aliased either 'concernOwner', // If the Concern defines any hidden properties or methods, @@ -64,6 +64,10 @@ export const ALWAYS_HIDDEN: ReadonlyArray = [ // Other properties and methods: // ----------------------------------------------------------------- // + // Object "prototype" property is too dangerous to tamper with, + // within the context of aliasing! + 'prototype', + // In case that a concern class uses other concerns, prevent them // from being aliased. CONCERNS, From ed4818ec472106cb29d4f4825effebb9288310ea Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 13:13:31 +0100 Subject: [PATCH 044/424] Exclude contracts --- packages/support/rollup.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/support/rollup.config.mjs b/packages/support/rollup.config.mjs index bf68caf7..4f6c2cc1 100644 --- a/packages/support/rollup.config.mjs +++ b/packages/support/rollup.config.mjs @@ -3,6 +3,7 @@ import { createConfig } from '../../shared/rollup.config.mjs'; export default createConfig({ baseDir: new URL('.', import.meta.url), external: [ + '@aedart/contracts', '@aedart/contracts/support', '@aedart/contracts/support/concerns', '@aedart/contracts/support/meta', From b935f874b1c5426452803c68dcbc4fb92a90b090 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 13:14:27 +0100 Subject: [PATCH 045/424] Add get class property descriptor Should return descriptors of keys defined in target's prototype --- .../reflections/getClassPropertyDescriptor.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 packages/support/src/reflections/getClassPropertyDescriptor.ts diff --git a/packages/support/src/reflections/getClassPropertyDescriptor.ts b/packages/support/src/reflections/getClassPropertyDescriptor.ts new file mode 100644 index 00000000..aa351b65 --- /dev/null +++ b/packages/support/src/reflections/getClassPropertyDescriptor.ts @@ -0,0 +1,20 @@ +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; + +/** + * Returns a {@link PropertyDescriptor} object, from target class' prototype that matches given property key + * + * @param {ConstructorOrAbstractConstructor} target Class that contains property in its prototype + * @param {PropertyKey} key Name of the property + * + * @return {PropertyDescriptor|undefined} Property descriptor or `undefined` if property does + * not exist in target's prototype. + * + * @throws {TypeError} If target is not an object or has no prototype + */ +export function getClassPropertyDescriptor(target: ConstructorOrAbstractConstructor, key: PropertyKey): PropertyDescriptor|undefined +{ + return Reflect.getOwnPropertyDescriptor( + Reflect.getPrototypeOf(target), + key + ); +} \ No newline at end of file From a9ded2a82f2daf89e1c68ecad1ff9cc56bb923e5 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 13:14:37 +0100 Subject: [PATCH 046/424] Export get class property descriptor --- packages/support/src/reflections/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index 452ca9ca..df8c095b 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -1,3 +1,4 @@ +export * from './getClassPropertyDescriptor'; export * from './isCallable'; export * from './isClassConstructor'; export * from './isConstructor'; \ No newline at end of file From caf20833f1e7a307cd29c08b4bf605a71412c11a Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 13:45:32 +0100 Subject: [PATCH 047/424] Fix target prototype not used for obtaining descriptors --- .../support/src/reflections/getClassPropertyDescriptor.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/support/src/reflections/getClassPropertyDescriptor.ts b/packages/support/src/reflections/getClassPropertyDescriptor.ts index aa351b65..c8b6dfa0 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptor.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptor.ts @@ -1,7 +1,7 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; /** - * Returns a {@link PropertyDescriptor} object, from target class' prototype that matches given property key + * Returns a {@link PropertyDescriptor} object, from target's prototype that matches given property key * * @param {ConstructorOrAbstractConstructor} target Class that contains property in its prototype * @param {PropertyKey} key Name of the property @@ -13,8 +13,12 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; */ export function getClassPropertyDescriptor(target: ConstructorOrAbstractConstructor, key: PropertyKey): PropertyDescriptor|undefined { + if (typeof target['prototype'] === 'undefined') { + throw new TypeError('Target has not "prototype"'); + } + return Reflect.getOwnPropertyDescriptor( - Reflect.getPrototypeOf(target), + target.prototype, key ); } \ No newline at end of file From 428c23458264b09f11cce0d63b030247ee290a1b Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 13:45:54 +0100 Subject: [PATCH 048/424] Add unit tests for getClassPropertyDescriptor --- .../getClassPropertyDescriptor.test.js | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/browser/packages/support/reflections/getClassPropertyDescriptor.test.js diff --git a/tests/browser/packages/support/reflections/getClassPropertyDescriptor.test.js b/tests/browser/packages/support/reflections/getClassPropertyDescriptor.test.js new file mode 100644 index 00000000..2a1aee78 --- /dev/null +++ b/tests/browser/packages/support/reflections/getClassPropertyDescriptor.test.js @@ -0,0 +1,78 @@ +import {getClassPropertyDescriptor} from "@aedart/support/reflections"; + +fdescribe('@aedart/support/reflections', () => { + + describe('getClassPropertyDescriptor()', () => { + + it('fails when target has no prototype', () => { + const callback = () => { + const obj = Object.create(null); + + getClassPropertyDescriptor(obj, 'name'); + } + + expect(callback) + .withContext('Should not be able to obtain anything from object without prototype') + .toThrowError(TypeError); + }); + + it('returns undefined if property does not exist', () => { + class A {} + + const descriptor = getClassPropertyDescriptor(A, 'unknown_property'); + expect(descriptor) + .toBeUndefined(); + }); + + it('can get property descriptor from target prototype', () => { + + const MY_SYMBOL = Symbol('my_symbol'); + + class A { + set name(v) {} + get name() {} + foo() {} + [MY_SYMBOL]() {} + } + + const properties = [ + 'name', + 'foo', + MY_SYMBOL + ]; + + for (const key of properties) { + const descriptor = getClassPropertyDescriptor(A, key); + + // Debug + // console.log(descriptor); + + let k = (typeof key == "symbol") + ? key.toString() + : key + + expect(descriptor) + .withContext('No descriptor returned for ' + k) + .not + .toBeUndefined() + } + }); + + it('returns undefined if property is private', () => { + class A { + #foo() {} + } + + const a = getClassPropertyDescriptor(A, 'foo'); + expect(a) + .withContext('Returned descriptor for "foo"') + .toBeUndefined(); + + const b = getClassPropertyDescriptor(A, '#foo'); + expect(b) + .withContext('Returned descriptor for "#foo"') + .toBeUndefined(); + }); + }); + +}); \ No newline at end of file From 5ca42d1b69bb1cb901c28238e6681f2fc547b1d2 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 13:48:30 +0100 Subject: [PATCH 049/424] Change release notes --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c65be63..e3792628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +* `getClassPropertyDescriptor()` in `@aedart/support/reflections`. + ## [0.8.0] - 2024-02-12 ### Added From 491aba060c2e9396e0e96620f89cfa9f25a995b0 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 14:21:30 +0100 Subject: [PATCH 050/424] Cleanup --- .../support/reflections/getClassPropertyDescriptor.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/packages/support/reflections/getClassPropertyDescriptor.test.js b/tests/browser/packages/support/reflections/getClassPropertyDescriptor.test.js index 2a1aee78..0218777d 100644 --- a/tests/browser/packages/support/reflections/getClassPropertyDescriptor.test.js +++ b/tests/browser/packages/support/reflections/getClassPropertyDescriptor.test.js @@ -1,6 +1,6 @@ import {getClassPropertyDescriptor} from "@aedart/support/reflections"; -fdescribe('@aedart/support/reflections', () => { +describe('@aedart/support/reflections', () => { describe('getClassPropertyDescriptor()', () => { From b37249ac6ed657f2481658320bedcae4e7fcbba6 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 15:48:03 +0100 Subject: [PATCH 051/424] Add get class property descriptors --- .../getClassPropertyDescriptors.ts | 62 ++++++ packages/support/src/reflections/index.ts | 1 + .../getClassPropertyDescriptors.test.js | 183 ++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 packages/support/src/reflections/getClassPropertyDescriptors.ts create mode 100644 tests/browser/packages/support/reflections/getClassPropertyDescriptors.test.js diff --git a/packages/support/src/reflections/getClassPropertyDescriptors.ts b/packages/support/src/reflections/getClassPropertyDescriptors.ts new file mode 100644 index 00000000..2243af3b --- /dev/null +++ b/packages/support/src/reflections/getClassPropertyDescriptors.ts @@ -0,0 +1,62 @@ +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { getClassPropertyDescriptor } from "./getClassPropertyDescriptor"; + +/** + * Returns all property descriptors that are defined target's prototype + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getOwnPropertyDescriptor + * + * @param {ConstructorOrAbstractConstructor} target The target class + * @param {boolean} [recursive=false] If `true`, then target's parent prototypes are traversed. + * Descriptors are merged, such that the top-most class' descriptors + * are returned. + * + * @return {Record} Object with the property descriptors, or empty object of target has + * properties defined. + * + * @throws {TypeError} If target is not an object or has no prototype + */ +export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstructor, recursive: boolean = false): Record +{ + if (typeof target['prototype'] === 'undefined') { + throw new TypeError('Target has not "prototype"'); + } + + // Define list of targets... + const targets = [target.prototype]; + + // If recursive flag is set, then all of target's parent classes must be obtained. + if (recursive) { + let parent = Reflect.getPrototypeOf(target.prototype); + const fnProto = Reflect.getPrototypeOf(Function); + + while(parent !== null && parent !== fnProto) { + targets.push(parent); + + parent = Reflect.getPrototypeOf(parent); + } + + // Reverse the targets, such that the top-most property descriptors are returned. + targets.reverse(); + } + + let output: Record = Object.create(null); + + // Obtain property descriptors for all targets + for (const t of targets) { + const keys: PropertyKey[] = Reflect.ownKeys(t); + for (const key: PropertyKey of keys) { + const descriptor: PropertyDescriptor = getClassPropertyDescriptor(t.constructor, key); + + // Merge evt. existing descriptor object with the one obtained from target. + if (Reflect.has(output, key)) { + output[key] = Object.assign(output[key], descriptor); + continue; + } + + output[key] = descriptor; + } + } + + return output; +} \ No newline at end of file diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index df8c095b..31c27f85 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -1,4 +1,5 @@ export * from './getClassPropertyDescriptor'; +export * from './getClassPropertyDescriptors'; export * from './isCallable'; export * from './isClassConstructor'; export * from './isConstructor'; \ No newline at end of file diff --git a/tests/browser/packages/support/reflections/getClassPropertyDescriptors.test.js b/tests/browser/packages/support/reflections/getClassPropertyDescriptors.test.js new file mode 100644 index 00000000..5bebac24 --- /dev/null +++ b/tests/browser/packages/support/reflections/getClassPropertyDescriptors.test.js @@ -0,0 +1,183 @@ +import {getClassPropertyDescriptor, getClassPropertyDescriptors} from "@aedart/support/reflections"; + + +fdescribe('@aedart/support/reflections', () => { + + describe('getClassPropertyDescriptors()', () => { + + it('fails when target has no prototype', () => { + const callback = () => { + const obj = Object.create(null); + + getClassPropertyDescriptors(obj); + } + + expect(callback) + .withContext('Should not be able to obtain anything from object without prototype') + .toThrowError(TypeError); + }); + + it('can get property descriptors for class', () => { + + const MY_SYMBOL = Symbol('my_symbol'); + + class A { + set name(v) {} + get name() {} + bar() {} + [MY_SYMBOL]() {} + } + + // -------------------------------------------------------------------------------- // + + const expected = [ + 'constructor', + 'name', + 'bar', + MY_SYMBOL + ]; + + const descriptors = getClassPropertyDescriptors(A); + + // Debug + // console.log(descriptors); + + for (const key of expected) { + let k = (typeof key == "symbol") + ? key.toString() + : key + + expect(Reflect.has(descriptors, key)) + .withContext('Key ' + k + ' not in descriptors record') + .toBeTrue(); + + const descriptor = descriptors[key]; + + // Debug + // console.log(key, descriptor); + + expect(descriptor) + .withContext('No descriptor returned for ' + k) + .not + .toBeUndefined(); + } + }); + + it('can get property descriptors for class recursively', () => { + + const MY_SYMBOL = Symbol('my_symbol'); + + class A { + set name(v) {} + get name() {} + foo() {} + [MY_SYMBOL]() {} + } + + class B extends A { + set bar(v) {} + get bar() {} + } + + // -------------------------------------------------------------------------------- // + + const expected = [ + 'constructor', + 'name', + 'foo', + 'bar', + MY_SYMBOL + ]; + + const descriptors = getClassPropertyDescriptors(B, true); + for (const key of expected) { + let k = (typeof key == "symbol") + ? key.toString() + : key + + expect(Reflect.has(descriptors, key)) + .withContext('Key ' + k + ' not in descriptors record') + .toBeTrue(); + + const descriptor = descriptors[key]; + expect(descriptor) + .withContext('No descriptor returned for ' + k) + .not + .toBeUndefined(); + } + }); + + it('returns top-most property descriptors', () => { + + const MY_SYMBOL = Symbol('my_symbol'); + + class A { + set name(v) {} + get name() {} + foo() {} + [MY_SYMBOL]() {} + } + + class B extends A { + set name(v) {} + get name() {} + foo() {} + [MY_SYMBOL]() { + return false; + } + } + + // -------------------------------------------------------------------------------- // + + const expected = [ + 'constructor', + 'name', + 'foo', + MY_SYMBOL + ]; + + const descriptors = getClassPropertyDescriptors(B, true); + for (const key of expected) { + let k = (typeof key == "symbol") + ? key.toString() + : key + + const parentDescriptor = getClassPropertyDescriptor(A, key); + const targetDescriptor = getClassPropertyDescriptor(B, key); + const descriptor = descriptors[key]; + + // Debug + // console.log('parent', key, parentDescriptor); + // console.log('target', key, descriptor); + + // Value, get, set... check of descriptor + const props = Reflect.ownKeys(descriptor); + for (const p of props) { + + // Debug + // console.log(' - parent', p, parentDescriptor[p]); + // console.log(' - target', p, descriptor[p]); + + // Skip assert if not "value", "get" or "set + if (!['value', 'get', 'set'].includes(p)) { + continue; + } + + // Ensure does not match parent's descriptor... + expect(descriptor[p] !== parentDescriptor[p]) + .withContext(`${k}[${p}] matches parent descriptor property, but SHOULD NOT do so`) + .toBeTrue(); + + // Double check... + expect(descriptor[p] === targetDescriptor[p]) + .withContext(`${k}[${p}] does NOT match target property descriptor property!`) + .toBeTrue(); + } + + // Debug + // console.log('- - - '.repeat(15)); + } + }); + }); + +}); \ No newline at end of file From cf4ee0adbe12ed28094e703a7d178ee29e4bbb9d Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 15:48:15 +0100 Subject: [PATCH 052/424] Add reference to Mozilla docs --- packages/support/src/reflections/getClassPropertyDescriptor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/support/src/reflections/getClassPropertyDescriptor.ts b/packages/support/src/reflections/getClassPropertyDescriptor.ts index c8b6dfa0..14b8e60c 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptor.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptor.ts @@ -3,6 +3,8 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; /** * Returns a {@link PropertyDescriptor} object, from target's prototype that matches given property key * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getOwnPropertyDescriptor + * * @param {ConstructorOrAbstractConstructor} target Class that contains property in its prototype * @param {PropertyKey} key Name of the property * From b4d60cb8481bb24b3e947a9f206bf7a9c3a938c1 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 15:48:28 +0100 Subject: [PATCH 053/424] Cleanup --- .../support/reflections/getClassPropertyDescriptors.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/browser/packages/support/reflections/getClassPropertyDescriptors.test.js b/tests/browser/packages/support/reflections/getClassPropertyDescriptors.test.js index 5bebac24..1e2f63ed 100644 --- a/tests/browser/packages/support/reflections/getClassPropertyDescriptors.test.js +++ b/tests/browser/packages/support/reflections/getClassPropertyDescriptors.test.js @@ -1,7 +1,6 @@ import {getClassPropertyDescriptor, getClassPropertyDescriptors} from "@aedart/support/reflections"; - -fdescribe('@aedart/support/reflections', () => { +describe('@aedart/support/reflections', () => { describe('getClassPropertyDescriptors()', () => { From 2f7f1abac0fec71efefcfd7978a89e455249cd3a Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 15:50:54 +0100 Subject: [PATCH 054/424] Fix style --- packages/support/src/reflections/getClassPropertyDescriptors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/reflections/getClassPropertyDescriptors.ts b/packages/support/src/reflections/getClassPropertyDescriptors.ts index 2243af3b..3860978c 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptors.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptors.ts @@ -40,7 +40,7 @@ export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstru targets.reverse(); } - let output: Record = Object.create(null); + const output: Record = Object.create(null); // Obtain property descriptors for all targets for (const t of targets) { From 5945c4ee726eb0bdedffe15d48f20132a0504657 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 15 Feb 2024 15:51:24 +0100 Subject: [PATCH 055/424] Change release notes --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3792628..b393cfa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* `getClassPropertyDescriptor()` in `@aedart/support/reflections`. +* `getClassPropertyDescriptor()` and `getClassPropertyDescriptors()` in `@aedart/support/reflections`. ## [0.8.0] - 2024-02-12 From 8b785fcf70b5480bfaea4c8bdfbd7d30a466951f Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 15 Feb 2024 18:18:33 +0100 Subject: [PATCH 056/424] Add prototype of Function as const in contracts --- packages/contracts/src/support/reflections/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/contracts/src/support/reflections/index.ts b/packages/contracts/src/support/reflections/index.ts index a6fdf909..646b1c2e 100644 --- a/packages/contracts/src/support/reflections/index.ts +++ b/packages/contracts/src/support/reflections/index.ts @@ -4,3 +4,10 @@ * @type {Symbol} */ export const SUPPORT_REFLECTIONS: unique symbol = Symbol('@aedart/contracts/support/reflections'); + +/** + * The prototype of {@link Function} + * + * @type {object} + */ +export const FUNCTION_PROTOTYPE: object = Reflect.getPrototypeOf(Function); From e46510890cb41cbbd9396f501ec232b1eea86392 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 15 Feb 2024 18:21:55 +0100 Subject: [PATCH 057/424] Refactor, use FUNCTION_PROTOTYPE const Also, handle edge case where property descriptor is undefined for a key --- .../src/reflections/getClassPropertyDescriptors.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/support/src/reflections/getClassPropertyDescriptors.ts b/packages/support/src/reflections/getClassPropertyDescriptors.ts index 3860978c..9dd05902 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptors.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptors.ts @@ -1,4 +1,5 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { FUNCTION_PROTOTYPE } from "@aedart/contracts/support/reflections"; import { getClassPropertyDescriptor } from "./getClassPropertyDescriptor"; /** @@ -28,9 +29,8 @@ export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstru // If recursive flag is set, then all of target's parent classes must be obtained. if (recursive) { let parent = Reflect.getPrototypeOf(target.prototype); - const fnProto = Reflect.getPrototypeOf(Function); - while(parent !== null && parent !== fnProto) { + while(parent !== null && parent !== FUNCTION_PROTOTYPE) { targets.push(parent); parent = Reflect.getPrototypeOf(parent); @@ -46,7 +46,11 @@ export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstru for (const t of targets) { const keys: PropertyKey[] = Reflect.ownKeys(t); for (const key: PropertyKey of keys) { - const descriptor: PropertyDescriptor = getClassPropertyDescriptor(t.constructor, key); + const descriptor: PropertyDescriptor | undefined = getClassPropertyDescriptor(t.constructor, key); + if (descriptor === undefined) { + output[key] = undefined; + continue; + } // Merge evt. existing descriptor object with the one obtained from target. if (Reflect.has(output, key)) { From 9cf603f79e58dd70a3991619ddb54fa4d2c34566 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 15 Feb 2024 18:26:12 +0100 Subject: [PATCH 058/424] Refactor findAddress, use FUNCTION_PROTOTYPE const --- packages/support/src/meta/targetMeta.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/support/src/meta/targetMeta.ts b/packages/support/src/meta/targetMeta.ts index 931a1970..a89390a5 100644 --- a/packages/support/src/meta/targetMeta.ts +++ b/packages/support/src/meta/targetMeta.ts @@ -5,6 +5,7 @@ import type { MetaEntry, MetaAddress, } from "@aedart/contracts/support/meta"; +import { FUNCTION_PROTOTYPE } from "@aedart/contracts/support/reflections"; import { METADATA, TARGET_METADATA, Kind } from "@aedart/contracts/support/meta"; import { mergeKeys, empty } from "@aedart/support/misc"; import { meta, getMeta } from './meta' @@ -244,15 +245,12 @@ function findAddress(target: object): MetaAddress | undefined target = target.constructor; } - // Obtain the prototype of Function... - const functionProto: object|null = Reflect.getPrototypeOf(Function); - // 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 === functionProto) { + if (parent === null || parent === FUNCTION_PROTOTYPE) { break; } From 016bbed61e007307b7c48066b78bac1bb818dc77 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 15 Feb 2024 18:33:36 +0100 Subject: [PATCH 059/424] Add reference to Mozilla docs --- packages/contracts/src/support/reflections/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contracts/src/support/reflections/index.ts b/packages/contracts/src/support/reflections/index.ts index 646b1c2e..c39055a0 100644 --- a/packages/contracts/src/support/reflections/index.ts +++ b/packages/contracts/src/support/reflections/index.ts @@ -8,6 +8,8 @@ export const SUPPORT_REFLECTIONS: unique symbol = Symbol('@aedart/contracts/supp /** * The prototype of {@link Function} * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/prototype + * * @type {object} */ export const FUNCTION_PROTOTYPE: object = Reflect.getPrototypeOf(Function); From a91c0bd5cbfeaff0081f0effb40f165ac1764d6f Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 15 Feb 2024 18:43:33 +0100 Subject: [PATCH 060/424] Add hasPrototype util function --- .../support/src/reflections/hasPrototype.ts | 11 ++++++++++ packages/support/src/reflections/index.ts | 1 + .../support/reflections/hasPrototype.test.js | 21 +++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 packages/support/src/reflections/hasPrototype.ts create mode 100644 tests/browser/packages/support/reflections/hasPrototype.test.js diff --git a/packages/support/src/reflections/hasPrototype.ts b/packages/support/src/reflections/hasPrototype.ts new file mode 100644 index 00000000..13cb00e6 --- /dev/null +++ b/packages/support/src/reflections/hasPrototype.ts @@ -0,0 +1,11 @@ +/** + * Determine if target object has a prototype property defined and that prototype is an object + * + * @param {object} target + * + * @returns {boolean} + */ +export function hasPrototype(target: object): boolean +{ + return Reflect.has(target, 'prototype') && typeof target.prototype == 'object'; +} \ No newline at end of file diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index 31c27f85..de692a9b 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -1,5 +1,6 @@ export * from './getClassPropertyDescriptor'; export * from './getClassPropertyDescriptors'; +export * from './hasPrototype'; export * from './isCallable'; export * from './isClassConstructor'; export * from './isConstructor'; \ No newline at end of file diff --git a/tests/browser/packages/support/reflections/hasPrototype.test.js b/tests/browser/packages/support/reflections/hasPrototype.test.js new file mode 100644 index 00000000..4f3840b9 --- /dev/null +++ b/tests/browser/packages/support/reflections/hasPrototype.test.js @@ -0,0 +1,21 @@ +import {hasPrototype} from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('hasPrototype()', () => { + + it('can determine if object has prototype', () => { + + class A {} + const obj = Object.create(null); + + expect(hasPrototype(A)) + .withContext('Class A should have a prototype') + .toBeTrue(); + + expect(hasPrototype(obj)) + .withContext('Object.create(null) should NOT have a prototype') + .toBeFalse(); + }); + + }); +}); \ No newline at end of file From e2777157606787721cb2ffa9ba76092d6d141446 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 15 Feb 2024 19:32:23 +0100 Subject: [PATCH 061/424] Redesign hasPrototype Now named hasPrototypeProperty, to reduce confusion. Also, ensuring that prototype property is neither null nor undefined. --- .../support/src/reflections/hasPrototype.ts | 11 ---- .../src/reflections/hasPrototypeProperty.ts | 18 ++++++ packages/support/src/reflections/index.ts | 2 +- .../support/reflections/hasPrototype.test.js | 21 ------- .../reflections/hasPrototypeProperty.test.js | 56 +++++++++++++++++++ 5 files changed, 75 insertions(+), 33 deletions(-) delete mode 100644 packages/support/src/reflections/hasPrototype.ts create mode 100644 packages/support/src/reflections/hasPrototypeProperty.ts delete mode 100644 tests/browser/packages/support/reflections/hasPrototype.test.js create mode 100644 tests/browser/packages/support/reflections/hasPrototypeProperty.test.js diff --git a/packages/support/src/reflections/hasPrototype.ts b/packages/support/src/reflections/hasPrototype.ts deleted file mode 100644 index 13cb00e6..00000000 --- a/packages/support/src/reflections/hasPrototype.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Determine if target object has a prototype property defined and that prototype is an object - * - * @param {object} target - * - * @returns {boolean} - */ -export function hasPrototype(target: object): boolean -{ - return Reflect.has(target, 'prototype') && typeof target.prototype == 'object'; -} \ No newline at end of file diff --git a/packages/support/src/reflections/hasPrototypeProperty.ts b/packages/support/src/reflections/hasPrototypeProperty.ts new file mode 100644 index 00000000..1a323144 --- /dev/null +++ b/packages/support/src/reflections/hasPrototypeProperty.ts @@ -0,0 +1,18 @@ +/** + * Determine if target object has a prototype property defined + * + * **Warning**: _This method is NOT the same as checking if {@link Reflect.getPrototypeOf} of an object is `null`!_ + * _The method literally checks if a "prototype" property is defined in target, that it is not `null` or `undefined`, + * and that its of the [type]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof} 'object'!_ + * + * @param {object} target + * + * @returns {boolean} + */ +export function hasPrototypeProperty(target: object): boolean +{ + return Reflect.has(target, 'prototype') + && target.prototype !== undefined + && target.prototype !== null + && typeof target.prototype == 'object'; +} \ No newline at end of file diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index de692a9b..630f2676 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -1,6 +1,6 @@ export * from './getClassPropertyDescriptor'; export * from './getClassPropertyDescriptors'; -export * from './hasPrototype'; +export * from './hasPrototypeProperty'; export * from './isCallable'; export * from './isClassConstructor'; export * from './isConstructor'; \ No newline at end of file diff --git a/tests/browser/packages/support/reflections/hasPrototype.test.js b/tests/browser/packages/support/reflections/hasPrototype.test.js deleted file mode 100644 index 4f3840b9..00000000 --- a/tests/browser/packages/support/reflections/hasPrototype.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import {hasPrototype} from "@aedart/support/reflections"; - -describe('@aedart/support/reflections', () => { - describe('hasPrototype()', () => { - - it('can determine if object has prototype', () => { - - class A {} - const obj = Object.create(null); - - expect(hasPrototype(A)) - .withContext('Class A should have a prototype') - .toBeTrue(); - - expect(hasPrototype(obj)) - .withContext('Object.create(null) should NOT have a prototype') - .toBeFalse(); - }); - - }); -}); \ No newline at end of file diff --git a/tests/browser/packages/support/reflections/hasPrototypeProperty.test.js b/tests/browser/packages/support/reflections/hasPrototypeProperty.test.js new file mode 100644 index 00000000..5c4655f9 --- /dev/null +++ b/tests/browser/packages/support/reflections/hasPrototypeProperty.test.js @@ -0,0 +1,56 @@ +import {hasPrototypeProperty} from "@aedart/support/reflections"; + +fdescribe('@aedart/support/reflections', () => { + describe('hasPrototypeProperty()', () => { + + it('can determine if object has prototype', () => { + const obj = Object.create({ prototype: {} }); + const objWithProto = { __proto__: function() {} }; + + const nullObj = Object.create(null); + const objWithUndefinedProto = { __proto__: undefined }; + const objWithPrototypeNull = { prototype: null }; + + expect(hasPrototypeProperty(obj)) + .withContext('object from Object.create({ prototype: {} }) should have a prototype') + .toBeTrue(); + + expect(hasPrototypeProperty(objWithProto)) + .withContext('object with __proto__ should have a prototype') + .toBeTrue(); + + expect(hasPrototypeProperty(nullObj)) + .withContext('object Object.create(null) should NOT have a prototype') + .toBeFalse(); + + expect(hasPrototypeProperty(objWithUndefinedProto)) + .withContext('object with __proto__:undefined should NOT have a prototype') + .toBeFalse(); + + expect(hasPrototypeProperty(objWithPrototypeNull)) + .withContext('object with prototype:null should NOT have a prototype') + .toBeFalse(); + }); + + it('can determine if function has prototype', () => { + const fn = function() {}; + const arrowFn = () => true; + + expect(hasPrototypeProperty(fn)) + .withContext('function should have a prototype') + .toBeTrue(); + + expect(hasPrototypeProperty(arrowFn)) + .withContext('arrow function should NOT have a prototype') + .toBeFalse(); + }); + + it('can determine if class has prototype', () => { + class A {} + + expect(hasPrototypeProperty(A)) + .withContext('Class A should have a prototype') + .toBeTrue(); + }); + }); +}); \ No newline at end of file From 950debcc6f1fbe6b485c736b13b85093fe980443 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 15 Feb 2024 19:41:53 +0100 Subject: [PATCH 062/424] Cleanup --- .../packages/support/reflections/hasPrototypeProperty.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/packages/support/reflections/hasPrototypeProperty.test.js b/tests/browser/packages/support/reflections/hasPrototypeProperty.test.js index 5c4655f9..e34156fd 100644 --- a/tests/browser/packages/support/reflections/hasPrototypeProperty.test.js +++ b/tests/browser/packages/support/reflections/hasPrototypeProperty.test.js @@ -1,6 +1,6 @@ import {hasPrototypeProperty} from "@aedart/support/reflections"; -fdescribe('@aedart/support/reflections', () => { +describe('@aedart/support/reflections', () => { describe('hasPrototypeProperty()', () => { it('can determine if object has prototype', () => { From bab125fa92234f974ea65e3d19eb47f05fd83ce7 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 15 Feb 2024 19:43:04 +0100 Subject: [PATCH 063/424] Add assertHasPrototypeProperty util function --- .../reflections/assertHasPrototypeProperty.ts | 18 +++++++++++ packages/support/src/reflections/index.ts | 1 + .../assertHasPrototypeProperty.test.js | 31 +++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 packages/support/src/reflections/assertHasPrototypeProperty.ts create mode 100644 tests/browser/packages/support/reflections/assertHasPrototypeProperty.test.js diff --git a/packages/support/src/reflections/assertHasPrototypeProperty.ts b/packages/support/src/reflections/assertHasPrototypeProperty.ts new file mode 100644 index 00000000..cf5e0006 --- /dev/null +++ b/packages/support/src/reflections/assertHasPrototypeProperty.ts @@ -0,0 +1,18 @@ +import { hasPrototypeProperty } from "./hasPrototypeProperty"; + +/** + * Assert that given target object has a "prototype" property defined + * + * @see hasPrototypeProperty + * + * @param {object} target + * @param {string} [message] + * + * @throws {TypeError} If target object does not have "prototype" property + */ +export function assertHasPrototypeProperty(target: object, message: string = 'target object has no "prototype" property'): void +{ + if (!hasPrototypeProperty(target)) { + throw new TypeError(message); + } +} \ No newline at end of file diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index 630f2676..b2235ebc 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -1,3 +1,4 @@ +export * from './assertHasPrototypeProperty'; export * from './getClassPropertyDescriptor'; export * from './getClassPropertyDescriptors'; export * from './hasPrototypeProperty'; diff --git a/tests/browser/packages/support/reflections/assertHasPrototypeProperty.test.js b/tests/browser/packages/support/reflections/assertHasPrototypeProperty.test.js new file mode 100644 index 00000000..6d32416f --- /dev/null +++ b/tests/browser/packages/support/reflections/assertHasPrototypeProperty.test.js @@ -0,0 +1,31 @@ +import { assertHasPrototypeProperty } from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('assertHasPrototypeProperty()', () => { + + it('does not throw when object has prototype property', () => { + + const callback = () => { + const obj = { __proto__: function() {} }; + + assertHasPrototypeProperty(obj); + } + + expect(callback) + .not + .toThrowError(TypeError) + }); + + it('throws when object has no prototype property', () => { + + const callback = () => { + const obj = { __proto__: null }; + + assertHasPrototypeProperty(obj); + } + + expect(callback) + .toThrowError(TypeError) + }); + }); +}); \ No newline at end of file From a4498b3b17469dbbe5aec1ac6e756023e6cd17ff Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 15 Feb 2024 19:46:58 +0100 Subject: [PATCH 064/424] Refactor, use assert util function --- .../support/src/reflections/getClassPropertyDescriptor.ts | 5 ++--- .../support/src/reflections/getClassPropertyDescriptors.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/support/src/reflections/getClassPropertyDescriptor.ts b/packages/support/src/reflections/getClassPropertyDescriptor.ts index 14b8e60c..839a1a3d 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptor.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptor.ts @@ -1,4 +1,5 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { assertHasPrototypeProperty } from "./assertHasPrototypeProperty"; /** * Returns a {@link PropertyDescriptor} object, from target's prototype that matches given property key @@ -15,9 +16,7 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; */ export function getClassPropertyDescriptor(target: ConstructorOrAbstractConstructor, key: PropertyKey): PropertyDescriptor|undefined { - if (typeof target['prototype'] === 'undefined') { - throw new TypeError('Target has not "prototype"'); - } + assertHasPrototypeProperty(target); return Reflect.getOwnPropertyDescriptor( target.prototype, diff --git a/packages/support/src/reflections/getClassPropertyDescriptors.ts b/packages/support/src/reflections/getClassPropertyDescriptors.ts index 9dd05902..0d742f8d 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptors.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptors.ts @@ -1,6 +1,7 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import { FUNCTION_PROTOTYPE } from "@aedart/contracts/support/reflections"; import { getClassPropertyDescriptor } from "./getClassPropertyDescriptor"; +import { assertHasPrototypeProperty } from "./assertHasPrototypeProperty"; /** * Returns all property descriptors that are defined target's prototype @@ -19,9 +20,7 @@ import { getClassPropertyDescriptor } from "./getClassPropertyDescriptor"; */ export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstructor, recursive: boolean = false): Record { - if (typeof target['prototype'] === 'undefined') { - throw new TypeError('Target has not "prototype"'); - } + assertHasPrototypeProperty(target); // Define list of targets... const targets = [target.prototype]; From 9f89c857a76d7aef3627e8603d9d4b3fea1382f1 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 15 Feb 2024 20:04:44 +0100 Subject: [PATCH 065/424] Improve description of FUNCTION_PROTOTYPE const --- packages/contracts/src/support/reflections/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contracts/src/support/reflections/index.ts b/packages/contracts/src/support/reflections/index.ts index c39055a0..97bc7664 100644 --- a/packages/contracts/src/support/reflections/index.ts +++ b/packages/contracts/src/support/reflections/index.ts @@ -8,6 +8,8 @@ export const SUPPORT_REFLECTIONS: unique symbol = Symbol('@aedart/contracts/supp /** * The prototype of {@link Function} * + * **Note**: _Prototype is obtained via `Reflect.getPrototypeOf(Function)`_ + * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/prototype * * @type {object} From 0ffcdf4342d3adc4a6c35ed04d2b386a633f075d Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 15 Feb 2024 20:44:16 +0100 Subject: [PATCH 066/424] Add get parent(s) of target class util functions --- .../src/reflections/getAllParentsOfClass.ts | 36 +++++++++ .../src/reflections/getParentOfClass.ts | 29 +++++++ packages/support/src/reflections/index.ts | 2 + .../reflections/getAllParentsOfClass.test.js | 78 +++++++++++++++++++ .../reflections/getParentOfClass.test.js | 43 ++++++++++ 5 files changed, 188 insertions(+) create mode 100644 packages/support/src/reflections/getAllParentsOfClass.ts create mode 100644 packages/support/src/reflections/getParentOfClass.ts create mode 100644 tests/browser/packages/support/reflections/getAllParentsOfClass.test.js create mode 100644 tests/browser/packages/support/reflections/getParentOfClass.test.js diff --git a/packages/support/src/reflections/getAllParentsOfClass.ts b/packages/support/src/reflections/getAllParentsOfClass.ts new file mode 100644 index 00000000..bb79372b --- /dev/null +++ b/packages/support/src/reflections/getAllParentsOfClass.ts @@ -0,0 +1,36 @@ +import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { getParentOfClass } from "./getParentOfClass"; +import { isset } from "@aedart/support/misc"; + +/** + * Returns all parent classes of given target + * + * @see {getParentOfClass} + * + * @param {ConstructorOrAbstractConstructor} target The target class. + * @param {boolean} [includeTarget=false] If `true`, then given target is included in the output as the first element. + * + * @returns {ConstructorOrAbstractConstructor[]} List of parent classes, ordered by the top-most parent class first. + * + * @throws {TypeError} + */ +export function getAllParentsOfClass(target: ConstructorOrAbstractConstructor, includeTarget: boolean = false): ConstructorOrAbstractConstructor[] +{ + if (!isset(target)) { + throw new TypeError('getAllParentsOfClass() expects a target class as argument, undefined given'); + } + + const output: ConstructorOrAbstractConstructor[] = []; + if (includeTarget) { + output.push(target); + } + + let parent: ConstructorOrAbstractConstructor | null = getParentOfClass(target); + while (parent !== null) { + output.push(parent); + + parent = getParentOfClass(parent); + } + + return output; +} \ No newline at end of file diff --git a/packages/support/src/reflections/getParentOfClass.ts b/packages/support/src/reflections/getParentOfClass.ts new file mode 100644 index 00000000..c84acffb --- /dev/null +++ b/packages/support/src/reflections/getParentOfClass.ts @@ -0,0 +1,29 @@ +import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { FUNCTION_PROTOTYPE } from "@aedart/contracts/support/reflections"; +import {isset} from "@aedart/support/misc"; + +/** + * Returns the parent class of given target class + * + * **Note**: _If target has a parent that matches + * [FUNCTION_PROTOTYPE]{@link import('@aedart/contracts/support/reflections').FUNCTION_PROTOTYPE}, then `null` is returned!_ + * + * @param {ConstructorOrAbstractConstructor} target The target class + * + * @returns {ConstructorOrAbstractConstructor | null} Parent class or `null`, if target has no parent class. + * + * @throws {TypeError} + */ +export function getParentOfClass(target: ConstructorOrAbstractConstructor): ConstructorOrAbstractConstructor | null +{ + if (!isset(target)) { + throw new TypeError('getParentOfClass() expects a target class as argument, undefined given'); + } + + const parent: object | null = Reflect.getPrototypeOf(target); + if (parent === FUNCTION_PROTOTYPE) { + return null; + } + + return parent as ConstructorOrAbstractConstructor; +} \ No newline at end of file diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index b2235ebc..6bd29b5e 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -1,6 +1,8 @@ export * from './assertHasPrototypeProperty'; +export * from './getAllParentsOfClass'; export * from './getClassPropertyDescriptor'; export * from './getClassPropertyDescriptors'; +export * from './getParentOfClass'; export * from './hasPrototypeProperty'; export * from './isCallable'; export * from './isClassConstructor'; diff --git a/tests/browser/packages/support/reflections/getAllParentsOfClass.test.js b/tests/browser/packages/support/reflections/getAllParentsOfClass.test.js new file mode 100644 index 00000000..07d4aac3 --- /dev/null +++ b/tests/browser/packages/support/reflections/getAllParentsOfClass.test.js @@ -0,0 +1,78 @@ +import { getAllParentsOfClass } from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('getAllParentsOfClass()', () => { + + it('fails when no arguments given', () => { + const callback = () => { + return getAllParentsOfClass(); + } + + expect(callback) + .toThrowError(TypeError); + }); + + it('returns all parent classes', () => { + + class A {} + class B extends A {} + class C extends B {} + + const parents = getAllParentsOfClass(C); + + // Debug + // console.log('parents of C', parents); + + expect(parents.length) + .withContext('Incorrect amount of parents returned') + .toEqual(2); + + expect(parents[0]) + .withContext('Incorrect parent of C') + .toEqual(B); + + expect(parents[1]) + .withContext('Incorrect parent of B') + .toEqual(A); + }); + + it('includes target in output', () => { + + class A {} + class B extends A {} + class C extends B {} + + const parents = getAllParentsOfClass(C, true); + + // Debug + // console.log('parents', parents); + + expect(parents.length) + .withContext('Incorrect amount of parents returned') + .toEqual(3); + + expect(parents[0]) + .withContext('First element should be C') + .toEqual(C); + + expect(parents[1]) + .withContext('Incorrect parent of C') + .toEqual(B); + + expect(parents[2]) + .withContext('Incorrect parent of B') + .toEqual(A); + }); + + it('returns empty array when target has no parents', () => { + + class A {} + + const parents = getAllParentsOfClass(A); + + expect(parents.length) + .withContext('A should not have any parents') + .toEqual(0); + }); + }); +}); \ No newline at end of file diff --git a/tests/browser/packages/support/reflections/getParentOfClass.test.js b/tests/browser/packages/support/reflections/getParentOfClass.test.js new file mode 100644 index 00000000..d82f2eee --- /dev/null +++ b/tests/browser/packages/support/reflections/getParentOfClass.test.js @@ -0,0 +1,43 @@ +import { getParentOfClass } from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('getParentOfClass()', () => { + + it('fails when no arguments given', () => { + const callback = () => { + return getParentOfClass(); + } + + expect(callback) + .toThrowError(TypeError); + }); + + it('can return parent class', () => { + + class A {} + class B extends A {} + class C extends B {} + + const parentOfC = getParentOfClass(C); + const parentOfB = getParentOfClass(B); + const parentOfA = getParentOfClass(A); + + // Debug + // console.log('Parent of C', parentOfC); + // console.log('Parent of B', parentOfB); + // console.log('Parent of A', parentOfA); + + expect(parentOfC) + .withContext('Incorrect parent of C') + .toEqual(B); + + expect(parentOfB) + .withContext('Incorrect parent of B') + .toEqual(A); + + expect(parentOfA) + .withContext('A should not have a parent') + .toBeNull() + }); + }); +}); \ No newline at end of file From 7f20ceb450e411d84519ff0c9d0bb0ef7dd48c59 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 15 Feb 2024 20:59:49 +0100 Subject: [PATCH 067/424] Refactor, use get all parents util function Also, removed edge case handling. In case that a property descriptor returns null, we simply skip it. --- .../getClassPropertyDescriptors.ts | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/support/src/reflections/getClassPropertyDescriptors.ts b/packages/support/src/reflections/getClassPropertyDescriptors.ts index 0d742f8d..fe7cb062 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptors.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptors.ts @@ -1,7 +1,7 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; -import { FUNCTION_PROTOTYPE } from "@aedart/contracts/support/reflections"; import { getClassPropertyDescriptor } from "./getClassPropertyDescriptor"; import { assertHasPrototypeProperty } from "./assertHasPrototypeProperty"; +import { getAllParentsOfClass } from "./getAllParentsOfClass"; /** * Returns all property descriptors that are defined target's prototype @@ -16,27 +16,18 @@ import { assertHasPrototypeProperty } from "./assertHasPrototypeProperty"; * @return {Record} Object with the property descriptors, or empty object of target has * properties defined. * - * @throws {TypeError} If target is not an object or has no prototype + * @throws {TypeError} If target is not an object or has no prototype property */ export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstructor, recursive: boolean = false): Record { assertHasPrototypeProperty(target); // Define list of targets... - const targets = [target.prototype]; - - // If recursive flag is set, then all of target's parent classes must be obtained. - if (recursive) { - let parent = Reflect.getPrototypeOf(target.prototype); - - while(parent !== null && parent !== FUNCTION_PROTOTYPE) { - targets.push(parent); + let targets = [target.prototype]; - parent = Reflect.getPrototypeOf(parent); - } - - // Reverse the targets, such that the top-most property descriptors are returned. - targets.reverse(); + // Obtain target's parent classes, such that top-most descriptors can be returned, if needed. + if (recursive) { + targets = getAllParentsOfClass(target.prototype, true).reverse(); } const output: Record = Object.create(null); @@ -46,8 +37,9 @@ export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstru const keys: PropertyKey[] = Reflect.ownKeys(t); for (const key: PropertyKey of keys) { const descriptor: PropertyDescriptor | undefined = getClassPropertyDescriptor(t.constructor, key); + + // If for some reason we are unable to obtain a descriptor, then skip it. if (descriptor === undefined) { - output[key] = undefined; continue; } From 2c40da7c69b273ab304a5e35b923731b4623f253 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 15 Feb 2024 21:03:29 +0100 Subject: [PATCH 068/424] Change release notes --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b393cfa8..e5e5d36a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* `FUNCTION_PROTOTYPE` const in `@aedart/contracts/support/reflections`. +* `hasPrototypeProperty()` and `assertHasPrototypeProperty()` in `@aedart/support/reflections`. +* `getParentOfClass()` and `getAllParentsOfClass()` in `@aedart/support/reflections`. * `getClassPropertyDescriptor()` and `getClassPropertyDescriptors()` in `@aedart/support/reflections`. ## [0.8.0] - 2024-02-12 From 389f437aebd3f5d5b78facbf64ccc870a5e33712 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 09:03:56 +0100 Subject: [PATCH 069/424] Remove redundant undefined check --- packages/support/src/reflections/hasPrototypeProperty.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/support/src/reflections/hasPrototypeProperty.ts b/packages/support/src/reflections/hasPrototypeProperty.ts index 1a323144..cb0957ce 100644 --- a/packages/support/src/reflections/hasPrototypeProperty.ts +++ b/packages/support/src/reflections/hasPrototypeProperty.ts @@ -12,7 +12,6 @@ export function hasPrototypeProperty(target: object): boolean { return Reflect.has(target, 'prototype') - && target.prototype !== undefined && target.prototype !== null && typeof target.prototype == 'object'; } \ No newline at end of file From 26fba009711a71c39cd150f00f4a70b559807670 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 09:14:13 +0100 Subject: [PATCH 070/424] Fix typo --- packages/contracts/src/support/concerns/Injector.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index 9b634b3f..6381733e 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -76,9 +76,9 @@ export default interface Injector * * **Note**: _Method will do nothing if a property or method already exists in the target, with the same * name as the given alias!_ - * + * * @param {object} target The target class the alias must be created in - * @param {PropertyKey} key Name of the property or method in the source concern class to create alias for + * @param {PropertyKey} key Name of the property or method in the source concern class to create an alias for * @param {PropertyKey} alias Alias for the key to create in the target class (the proxy property or method) * @param {Constructor} source The concern to that the alias property or method must proxy to * From 709969b64fa481a7dc0b704d34b7005825562b09 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 09:36:17 +0100 Subject: [PATCH 071/424] Change return type of HIDDEN function --- packages/support/src/concerns/AbstractConcern.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index 48a88f05..dc491973 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -50,10 +50,10 @@ export default abstract class AbstractConcern implements Concern * an "injector" that injects this concern MUST ensure that the {@link ALWAYS_HIDDEN} * defined properties and methods are **NEVER** aliased into a target class._ * - * @return {PropertyKey[]} + * @return {ReadonlyArray} */ - static [HIDDEN](): PropertyKey[] + static [HIDDEN](): ReadonlyArray { - return ALWAYS_HIDDEN as PropertyKey[]; + return ALWAYS_HIDDEN; } } \ No newline at end of file From dbd4feb972d9a099fd7a6afc726cc04fcdd13bb2 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 09:40:38 +0100 Subject: [PATCH 072/424] Add unit tests for Abstract Concern class --- .../support/concerns/AbstractConcern.test.js | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tests/browser/packages/support/concerns/AbstractConcern.test.js diff --git a/tests/browser/packages/support/concerns/AbstractConcern.test.js b/tests/browser/packages/support/concerns/AbstractConcern.test.js new file mode 100644 index 00000000..f5a77de4 --- /dev/null +++ b/tests/browser/packages/support/concerns/AbstractConcern.test.js @@ -0,0 +1,69 @@ +import { AbstractConcern } from "@aedart/support/concerns"; +import { HIDDEN } from "@aedart/contracts/support/concerns"; + +describe('@aedart/support/concerns', () => { + + describe('AbstractConcern', () => { + + it('fails when attempting to create direct instance of Abstract Concern', () => { + const callback = () => { + return new AbstractConcern(class {}); + } + + expect(callback) + .toThrowError(Error); + }); + + it('can obtain concern owner instance', () => { + + class Owner {} + + class MyConcern extends AbstractConcern {} + + const owner = new Owner(); + const concern = new MyConcern(owner); + + const result = concern.concernOwner; + expect(result) + .toBe(owner); + }); + + it('returns default list of hidden properties and methods', () => { + + class MyConcern extends AbstractConcern {} + + const result = MyConcern[HIDDEN](); + + // Debug + // console.log('Hidden', result); + + expect(result.length) + .not + .toEqual(0) + }); + + it('can overwrite default hidden', () => { + + const newHidden = [ 'a', 'b', 'c' ]; + class MyConcern extends AbstractConcern { + static [HIDDEN]() + { + return newHidden; + } + } + + // --------------------------------------------------------------- /7 + + const result = MyConcern[HIDDEN](); + for (const key of result) { + const k = typeof key == 'symbol' + ? key.toString() + : key; + + expect(newHidden.includes(key)) + .withContext(`${k} not part of HIDDEN`) + .toBeTrue() + } + }); + }); +}); \ No newline at end of file From bc353fc6a26dfa4f372503c1839df790f0944d4e Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 10:11:14 +0100 Subject: [PATCH 073/424] Change return type of all() to iterator --- packages/contracts/src/support/concerns/Container.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index 6efdfed7..51af2a1c 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -97,7 +97,7 @@ export default interface Container /** * Returns all concern classes * - * @return {Constructor[]} + * @return {IterableIterator>} */ - all(): Constructor[]; + all(): IterableIterator>; } \ No newline at end of file From 4882e9e6850f80f9155f00a50fe9076c30f80baa Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 10:18:28 +0100 Subject: [PATCH 074/424] Change description of boot() Method shouldn't fail if concern was already booted. --- packages/contracts/src/support/concerns/Container.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index 51af2a1c..4755c2d3 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -61,15 +61,15 @@ export default interface Container /** * Boot concern class - * + * * @template T extends {@link Concern} * * @param {Constructor} concern * - * @return {Concern} New concern instance + * @return {Concern} New concern instance, or existing instance if concern has + * already been booted. * - * @throws {Error} If provided concern class has already been booted, or - * if not registered in this container. + * @throws {Error} If provided concern class is not registered in this container. */ boot(concern: Constructor): T; From 8ba85458e2868c7b56b5432998b11215e18901fc Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 10:25:52 +0100 Subject: [PATCH 075/424] Revert boot() description Ah - well, in this case its safer to allow the method to throw exception if concern was already booted. --- packages/contracts/src/support/concerns/Container.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index 4755c2d3..8cabafb2 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -66,10 +66,10 @@ export default interface Container * * @param {Constructor} concern * - * @return {Concern} New concern instance, or existing instance if concern has - * already been booted. + * @return {Concern} New concern instance * - * @throws {Error} If provided concern class is not registered in this container. + * @throws {Error} If provided concern class is not registered in this container, + * or if concern was already booted. */ boot(concern: Constructor): T; From 305a2124a4a116a2eec58321548b683223b2ae1b Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 11:19:46 +0100 Subject: [PATCH 076/424] Init contracts support exceptions submodule --- aliases.js | 1 + packages/contracts/package.json | 5 +++++ packages/contracts/rollup.config.mjs | 1 + packages/contracts/src/support/exceptions/index.ts | 6 ++++++ packages/support/rollup.config.mjs | 1 + 5 files changed, 14 insertions(+) create mode 100644 packages/contracts/src/support/exceptions/index.ts diff --git a/aliases.js b/aliases.js index 9b1958d8..b5856900 100644 --- a/aliases.js +++ b/aliases.js @@ -20,6 +20,7 @@ module.exports = { alias: { // contracts '@aedart/contracts/support/concerns': path.resolve(__dirname, './packages/contracts/support/concerns'), + '@aedart/contracts/support/exceptions': path.resolve(__dirname, './packages/contracts/support/exceptions'), '@aedart/contracts/support/meta': path.resolve(__dirname, './packages/contracts/support/meta'), '@aedart/contracts/support/mixins': path.resolve(__dirname, './packages/contracts/support/mixins'), '@aedart/contracts/support/reflections': path.resolve(__dirname, './packages/contracts/support/reflections'), diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 98d738e1..60c2e5ff 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -41,6 +41,11 @@ "import": "./dist/esm/support/concerns.js", "require": "./dist/cjs/support/concerns.cjs" }, + "./support/exceptions": { + "types": "./dist/types/support/exceptions.d.ts", + "import": "./dist/esm/support/exceptions.js", + "require": "./dist/cjs/support/exceptions.cjs" + }, "./support/meta": { "types": "./dist/types/support/meta.d.ts", "import": "./dist/esm/support/meta.js", diff --git a/packages/contracts/rollup.config.mjs b/packages/contracts/rollup.config.mjs index bf68caf7..9a56f5ce 100644 --- a/packages/contracts/rollup.config.mjs +++ b/packages/contracts/rollup.config.mjs @@ -5,6 +5,7 @@ export default createConfig({ external: [ '@aedart/contracts/support', '@aedart/contracts/support/concerns', + '@aedart/contracts/support/exceptions', '@aedart/contracts/support/meta', '@aedart/contracts/support/mixins', '@aedart/contracts/support/reflections', diff --git a/packages/contracts/src/support/exceptions/index.ts b/packages/contracts/src/support/exceptions/index.ts new file mode 100644 index 00000000..3400041a --- /dev/null +++ b/packages/contracts/src/support/exceptions/index.ts @@ -0,0 +1,6 @@ +/** + * Exceptions identifier + * + * @type {Symbol} + */ +export const SUPPORT_EXCEPTIONS: unique symbol = Symbol('@aedart/contracts/support/exceptions'); diff --git a/packages/support/rollup.config.mjs b/packages/support/rollup.config.mjs index 4f6c2cc1..a0d97e13 100644 --- a/packages/support/rollup.config.mjs +++ b/packages/support/rollup.config.mjs @@ -6,6 +6,7 @@ export default createConfig({ '@aedart/contracts', '@aedart/contracts/support', '@aedart/contracts/support/concerns', + '@aedart/contracts/support/exceptions', '@aedart/contracts/support/meta', '@aedart/contracts/support/mixins', '@aedart/contracts/support/reflections', From 0c919e39ba0bebf858c11feac74e0074034093e8 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 11:21:59 +0100 Subject: [PATCH 077/424] Init support exceptions submodule --- packages/support/package.json | 5 +++++ packages/support/src/exceptions/index.ts | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 packages/support/src/exceptions/index.ts diff --git a/packages/support/package.json b/packages/support/package.json index f7e5b377..56283270 100644 --- a/packages/support/package.json +++ b/packages/support/package.json @@ -33,6 +33,11 @@ "import": "./dist/esm/concerns.js", "require": "./dist/cjs/concerns.cjs" }, + "./exceptions": { + "types": "./dist/types/exceptions.d.ts", + "import": "./dist/esm/exceptions.js", + "require": "./dist/cjs/exceptions.cjs" + }, "./meta": { "types": "./dist/types/meta.d.ts", "import": "./dist/esm/meta.js", diff --git a/packages/support/src/exceptions/index.ts b/packages/support/src/exceptions/index.ts new file mode 100644 index 00000000..d3623e86 --- /dev/null +++ b/packages/support/src/exceptions/index.ts @@ -0,0 +1,4 @@ +/** + * TODO: replace this... + */ +export const TMP: string = 'TODO'; \ No newline at end of file From cdb5c21f46df1f63f2d0fa766efd94a914359b98 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 11:22:55 +0100 Subject: [PATCH 078/424] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5e5d36a..45599bc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* `@aedart/contracts/support/exceptions` and `@aedart/support/exceptions` submodules. * `FUNCTION_PROTOTYPE` const in `@aedart/contracts/support/reflections`. * `hasPrototypeProperty()` and `assertHasPrototypeProperty()` in `@aedart/support/reflections`. * `getParentOfClass()` and `getAllParentsOfClass()` in `@aedart/support/reflections`. From 8221e4ea9d87905db716a91601eea69f8cd07316 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 11:39:51 +0100 Subject: [PATCH 079/424] Add Throwable interface --- packages/contracts/src/support/exceptions/Throwable.ts | 10 ++++++++++ packages/contracts/src/support/exceptions/index.ts | 5 +++++ 2 files changed, 15 insertions(+) create mode 100644 packages/contracts/src/support/exceptions/Throwable.ts diff --git a/packages/contracts/src/support/exceptions/Throwable.ts b/packages/contracts/src/support/exceptions/Throwable.ts new file mode 100644 index 00000000..9c2b8cab --- /dev/null +++ b/packages/contracts/src/support/exceptions/Throwable.ts @@ -0,0 +1,10 @@ +/** + * Throwable + * + * Base interface for a custom {@link Error} that can be thrown. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + */ +export default interface Throwable extends Error +{} \ No newline at end of file diff --git a/packages/contracts/src/support/exceptions/index.ts b/packages/contracts/src/support/exceptions/index.ts index 3400041a..3c4481b0 100644 --- a/packages/contracts/src/support/exceptions/index.ts +++ b/packages/contracts/src/support/exceptions/index.ts @@ -4,3 +4,8 @@ * @type {Symbol} */ export const SUPPORT_EXCEPTIONS: unique symbol = Symbol('@aedart/contracts/support/exceptions'); + +import Throwable from "./Throwable"; +export { + type Throwable +} \ No newline at end of file From cfbfc9a762f94b6cdb1763134181720df94ef6a9 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 11:44:33 +0100 Subject: [PATCH 080/424] Add reference to PHP's Throwable --- packages/contracts/src/support/exceptions/Throwable.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/exceptions/Throwable.ts b/packages/contracts/src/support/exceptions/Throwable.ts index 9c2b8cab..d4e4714f 100644 --- a/packages/contracts/src/support/exceptions/Throwable.ts +++ b/packages/contracts/src/support/exceptions/Throwable.ts @@ -3,8 +3,10 @@ * * Base interface for a custom {@link Error} that can be thrown. * + * This interface is inspired by PHP's [`Throwable` interface]{@link https://www.php.net/manual/en/class.throwable.php}. + * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + * @see Error */ -export default interface Throwable extends Error -{} \ No newline at end of file +export default interface Throwable extends Error {} \ No newline at end of file From 90e03caf7a07b567fef39a2429f432c200a3bac9 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 12:17:14 +0100 Subject: [PATCH 081/424] Add Logical Error --- .../support/src/exceptions/LogicalError.ts | 29 +++++++++++++++++++ packages/support/src/exceptions/index.ts | 8 ++--- 2 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 packages/support/src/exceptions/LogicalError.ts diff --git a/packages/support/src/exceptions/LogicalError.ts b/packages/support/src/exceptions/LogicalError.ts new file mode 100644 index 00000000..b3e90fed --- /dev/null +++ b/packages/support/src/exceptions/LogicalError.ts @@ -0,0 +1,29 @@ +import type { Throwable } from "@aedart/contracts/support/exceptions"; + +/** + * Logical Error + * + * To be thrown whenever there is an error in the programming logic. + * + * This error is inspired by PHP's [`LogicException`]{@link https://www.php.net/manual/en/class.logicexception} + */ +export default class LogicalError extends Error implements Throwable +{ + /** + * Create a new logical error instance + * + * @param {string} [message] + * @param {ErrorOptions} [options] + */ + constructor(message?: string, options?: ErrorOptions) { + super(message, options); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, LogicalError); + } else { + this.stack = (new Error()).stack; + } + + this.name = "LogicalError"; + } +} \ No newline at end of file diff --git a/packages/support/src/exceptions/index.ts b/packages/support/src/exceptions/index.ts index d3623e86..acc51a37 100644 --- a/packages/support/src/exceptions/index.ts +++ b/packages/support/src/exceptions/index.ts @@ -1,4 +1,4 @@ -/** - * TODO: replace this... - */ -export const TMP: string = 'TODO'; \ No newline at end of file +import LogicalError from "./LogicalError"; +export { + LogicalError +} \ No newline at end of file From 1bd5b09d619e2b3d0ecc92555dd0fae75b5abf5f Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 12:18:27 +0100 Subject: [PATCH 082/424] Add unit test of Logical Error We are not going to test all future custom errors / exceptions like this. In this test, we just have to make sure that the specific properties and instance of check works as intended. --- .../support/exceptions/LogicalError.test.js | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/browser/packages/support/exceptions/LogicalError.test.js diff --git a/tests/browser/packages/support/exceptions/LogicalError.test.js b/tests/browser/packages/support/exceptions/LogicalError.test.js new file mode 100644 index 00000000..13f2941a --- /dev/null +++ b/tests/browser/packages/support/exceptions/LogicalError.test.js @@ -0,0 +1,64 @@ +import { LogicalError } from "@aedart/support/exceptions"; + +fdescribe('@aedart/support/exceptions', () => { + + describe('LogicalError', () => { + + it('can throw new Logical Error', () => { + + // Although we can use Jasmine's builtin helpers to capture, we do so manually + // for this custom error/exception to assert other aspects of it... + + const msg = 'Lorum Lipsum'; + const options = { cause: { details: 'Morus Esto buno' }}; + let wasThrown = false; + + try { + throw new LogicalError(msg, options); + } catch (error) { + // Debug + // console.log(error.toString(), error.cause, error.stack); + + wasThrown = true; + + expect(error.name) + .toBe('LogicalError'); + + expect(error.message) + .toBe(msg); + + expect(error.cause) + .withContext('Error cause does not appear to be defined') + .toBe(options.cause); + + expect(error.stack) + .withContext('Error stack does not appear to be defined') + .not + .toBeUndefined(); + + expect(error) + .withContext('Should be instance of LogicalError') + .toBeInstanceOf(LogicalError); + + expect(error) + .withContext('Should also be instance of Error') + .toBeInstanceOf(Error); + } + + expect(wasThrown) + .withContext('Custom error was not thrown') + .toBeTrue(); + }); + + it('can capture via expect', () => { + const callback = () => { + throw new LogicalError(); + } + + expect(callback) + .toThrowError(LogicalError); + }); + + }); + +}); \ No newline at end of file From 7bf512e0eb30e8ec32e5ab853c8f44ef56282729 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 12:18:56 +0100 Subject: [PATCH 083/424] Cleanup --- tests/browser/packages/support/exceptions/LogicalError.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/packages/support/exceptions/LogicalError.test.js b/tests/browser/packages/support/exceptions/LogicalError.test.js index 13f2941a..0a6209b6 100644 --- a/tests/browser/packages/support/exceptions/LogicalError.test.js +++ b/tests/browser/packages/support/exceptions/LogicalError.test.js @@ -1,6 +1,6 @@ import { LogicalError } from "@aedart/support/exceptions"; -fdescribe('@aedart/support/exceptions', () => { +describe('@aedart/support/exceptions', () => { describe('LogicalError', () => { From baae017e577c7c6d564d07cb9ef58f2795f50a62 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 13:07:12 +0100 Subject: [PATCH 084/424] Add getConstructorName() util function --- .../src/reflections/getConstructorName.ts | 23 +++++++++++ packages/support/src/reflections/index.ts | 1 + .../reflections/getConstructorName.test.js | 40 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 packages/support/src/reflections/getConstructorName.ts create mode 100644 tests/browser/packages/support/reflections/getConstructorName.test.js diff --git a/packages/support/src/reflections/getConstructorName.ts b/packages/support/src/reflections/getConstructorName.ts new file mode 100644 index 00000000..d9b20ee6 --- /dev/null +++ b/packages/support/src/reflections/getConstructorName.ts @@ -0,0 +1,23 @@ +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { isset } from "@aedart/support/objects"; + +/** + * Returns target class' constructor name, if available + * + * @param {ConstructorOrAbstractConstructor} target + * @param {string|null} [defaultValue=null] A default string value to return if target has no constructor name + * + * @return {string|null} Constructor name, or default value + */ +export function getConstructorName(target: ConstructorOrAbstractConstructor, defaultValue: string|null = null): string|null +{ + if (!isset(target, [ 'prototype', 'constructor', 'name' ])) { + return defaultValue; + } + + const name: string = target.prototype.constructor.name; + + return name.length > 0 + ? name + : defaultValue; +} \ No newline at end of file diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index 6bd29b5e..df705ee4 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -2,6 +2,7 @@ export * from './assertHasPrototypeProperty'; export * from './getAllParentsOfClass'; export * from './getClassPropertyDescriptor'; export * from './getClassPropertyDescriptors'; +export * from './getConstructorName'; export * from './getParentOfClass'; export * from './hasPrototypeProperty'; export * from './isCallable'; diff --git a/tests/browser/packages/support/reflections/getConstructorName.test.js b/tests/browser/packages/support/reflections/getConstructorName.test.js new file mode 100644 index 00000000..0bb3c95c --- /dev/null +++ b/tests/browser/packages/support/reflections/getConstructorName.test.js @@ -0,0 +1,40 @@ +import { getConstructorName } from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('getConstructorName()', () => { + + it('can obtain class constructor name', () => { + class Box {} + + const result = getConstructorName(Box); + + // Debug + console.log(result); + + expect(result) + .toBe('Box'); + }); + + it('returns null for anonymous class', () => { + const result = getConstructorName(class {}); + + // Debug + console.log(result); + + expect(result) + .toBeNull(); + }); + + it('returns default value', () => { + const defaultValue = 'MyBox'; + + const result = getConstructorName(class {}, defaultValue); + + // Debug + console.log(result); + + expect(result) + .toBe(defaultValue) + }); + }); +}); \ No newline at end of file From f4e3537f7fd69043119a5b4c1344687be34bd147 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 13:17:27 +0100 Subject: [PATCH 085/424] Cleanup --- .../packages/support/reflections/getConstructorName.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/browser/packages/support/reflections/getConstructorName.test.js b/tests/browser/packages/support/reflections/getConstructorName.test.js index 0bb3c95c..c4430bd9 100644 --- a/tests/browser/packages/support/reflections/getConstructorName.test.js +++ b/tests/browser/packages/support/reflections/getConstructorName.test.js @@ -9,7 +9,7 @@ describe('@aedart/support/reflections', () => { const result = getConstructorName(Box); // Debug - console.log(result); + // console.log(result); expect(result) .toBe('Box'); @@ -19,7 +19,7 @@ describe('@aedart/support/reflections', () => { const result = getConstructorName(class {}); // Debug - console.log(result); + // console.log(result); expect(result) .toBeNull(); @@ -31,7 +31,7 @@ describe('@aedart/support/reflections', () => { const result = getConstructorName(class {}, defaultValue); // Debug - console.log(result); + // console.log(result); expect(result) .toBe(defaultValue) From cfbafe97833406ac51f175ebeca12556c95ae4a4 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 13:18:30 +0100 Subject: [PATCH 086/424] Add getNameOrDesc() util function --- .../support/src/reflections/getNameOrDesc.ts | 23 +++++++++++++++ packages/support/src/reflections/index.ts | 1 + .../support/reflections/getNameOrDesc.test.js | 28 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 packages/support/src/reflections/getNameOrDesc.ts create mode 100644 tests/browser/packages/support/reflections/getNameOrDesc.test.js diff --git a/packages/support/src/reflections/getNameOrDesc.ts b/packages/support/src/reflections/getNameOrDesc.ts new file mode 100644 index 00000000..a7944004 --- /dev/null +++ b/packages/support/src/reflections/getNameOrDesc.ts @@ -0,0 +1,23 @@ +import type {ConstructorOrAbstractConstructor} from "@aedart/contracts"; +import { descTag } from "@aedart/support/misc"; +import { getConstructorName } from "./getConstructorName"; + +/** + * Return target class' constructor name or default to target's description tag if a name is unavailable + * + * **Note**: _Method is a shortcut for the following:_ + * ```js + * getConstructorName(target, descTag(target)); + * ``` + * + * @see getConstructorName + * @see descTag + * + * @param {ConstructorOrAbstractConstructor} target + * + * @return {string} + */ +export function getNameOrDesc(target: ConstructorOrAbstractConstructor): string +{ + return getConstructorName(target, descTag(target)); +} \ No newline at end of file diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index df705ee4..2c616ae9 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -3,6 +3,7 @@ export * from './getAllParentsOfClass'; export * from './getClassPropertyDescriptor'; export * from './getClassPropertyDescriptors'; export * from './getConstructorName'; +export * from './getNameOrDesc'; export * from './getParentOfClass'; export * from './hasPrototypeProperty'; export * from './isCallable'; diff --git a/tests/browser/packages/support/reflections/getNameOrDesc.test.js b/tests/browser/packages/support/reflections/getNameOrDesc.test.js new file mode 100644 index 00000000..b4a8bc52 --- /dev/null +++ b/tests/browser/packages/support/reflections/getNameOrDesc.test.js @@ -0,0 +1,28 @@ +import { getNameOrDesc } from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('getNameOrDesc()', () => { + + it('can obtain class constructor name', () => { + class ApiService {} + + const result = getNameOrDesc(ApiService); + + // Debug + // console.log(result); + + expect(result) + .toBe('ApiService'); + }); + + it('returns description tag for anonymous class', () => { + const result = getNameOrDesc(class {}); + + // Debug + // console.log(result); + + expect(result) + .toBe('[object Function]'); + }); + }); +}); \ No newline at end of file From 01a211a85f3564f8630e01c9071ab2fbbca51f83 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 13:23:24 +0100 Subject: [PATCH 087/424] Add Abstract Class Error exception --- .../src/exceptions/AbstractClassError.ts | 38 +++++++++++++ packages/support/src/exceptions/index.ts | 2 + .../exceptions/AbstractClassError.test.js | 53 +++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 packages/support/src/exceptions/AbstractClassError.ts create mode 100644 tests/browser/packages/support/exceptions/AbstractClassError.test.js diff --git a/packages/support/src/exceptions/AbstractClassError.ts b/packages/support/src/exceptions/AbstractClassError.ts new file mode 100644 index 00000000..6a6340e5 --- /dev/null +++ b/packages/support/src/exceptions/AbstractClassError.ts @@ -0,0 +1,38 @@ +import type { AbstractConstructor } from "@aedart/contracts"; +import LogicalError from "./LogicalError"; +import { getNameOrDesc } from "@aedart/support/reflections"; + +/** + * Abstract Class Error + * + * To be thrown whenever an abstract class is attempted instantiated directly. + */ +export default class AbstractClassError extends LogicalError +{ + /** + * The abstract class that was attempted instantiated + * + * @type {AbstractConstructor} + */ + target: AbstractConstructor; + + /** + * Create new instance of Abstract Class Error + * + * @param {AbstractConstructor} target The abstract class that was attempted instantiated directly + * @param {ErrorOptions} [options] + */ + constructor(target: AbstractConstructor, options?: ErrorOptions) { + super(`Unable to create new instance of abstract class ${getNameOrDesc(target)}`, options || { cause: { target: target } }); + + this.target = target; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, AbstractClassError); + } else { + this.stack = (new Error()).stack; + } + + this.name = "AbstractClassError"; + } +} \ No newline at end of file diff --git a/packages/support/src/exceptions/index.ts b/packages/support/src/exceptions/index.ts index acc51a37..ccd1f3e2 100644 --- a/packages/support/src/exceptions/index.ts +++ b/packages/support/src/exceptions/index.ts @@ -1,4 +1,6 @@ +import AbstractClassError from "./AbstractClassError"; import LogicalError from "./LogicalError"; export { + AbstractClassError, LogicalError } \ No newline at end of file diff --git a/tests/browser/packages/support/exceptions/AbstractClassError.test.js b/tests/browser/packages/support/exceptions/AbstractClassError.test.js new file mode 100644 index 00000000..3328981c --- /dev/null +++ b/tests/browser/packages/support/exceptions/AbstractClassError.test.js @@ -0,0 +1,53 @@ +import { AbstractClassError, LogicalError } from "@aedart/support/exceptions"; + +describe('@aedart/support/exceptions', () => { + + describe('AbstractClassError', () => { + + it('can throw new Abstract Class Error', () => { + + class A {} + let wasThrown = false; + + try { + throw new AbstractClassError(A); + } catch (error) { + // Debug + // console.log(error.toString(), error.cause, error.stack); + + wasThrown = true; + + expect(error.target) + .withContext('Target not set') + .toBe(A); + + expect(error) + .withContext('Should be instance of AbstractClassError') + .toBeInstanceOf(AbstractClassError); + + expect(error) + .withContext('Should also be instance of LogicalError') + .toBeInstanceOf(LogicalError); + + expect(error) + .withContext('Should also be instance of Error') + .toBeInstanceOf(Error); + } + + expect(wasThrown) + .withContext('Custom error was not thrown') + .toBeTrue(); + }); + + it('can capture via expect', () => { + const callback = () => { + throw new AbstractClassError(class {}); + } + + expect(callback) + .toThrowError(AbstractClassError); + }); + + }); + +}); \ No newline at end of file From df8ac8473d1a2f9eeed038e5f0999a2346181d8b Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 13:23:34 +0100 Subject: [PATCH 088/424] Change release notes --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45599bc3..ef2ed5b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * `@aedart/contracts/support/exceptions` and `@aedart/support/exceptions` submodules. +* `Throwable` (_extends TypeScript's `Error` interface_) interface in `@aedart/contracts/support/exceptions`. +* `LogicalError` and `AbstractClassError` exceptions in `@aedart/support/exceptions`. * `FUNCTION_PROTOTYPE` const in `@aedart/contracts/support/reflections`. * `hasPrototypeProperty()` and `assertHasPrototypeProperty()` in `@aedart/support/reflections`. * `getParentOfClass()` and `getAllParentsOfClass()` in `@aedart/support/reflections`. * `getClassPropertyDescriptor()` and `getClassPropertyDescriptors()` in `@aedart/support/reflections`. +* `getConstructorName()` and `getNameOrDesc()` in `@aedart/support/reflections`. ## [0.8.0] - 2024-02-12 From c0a0e2bcb0e136d866a9a7c7ec8dd94d41b13576 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 13:27:02 +0100 Subject: [PATCH 089/424] Change the exception to "Abstract Class Error" in class constructor --- packages/support/src/concerns/AbstractConcern.ts | 3 ++- .../browser/packages/support/concerns/AbstractConcern.test.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index dc491973..9f87a9f9 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -1,5 +1,6 @@ import type { Concern } from "@aedart/contracts/support/concerns"; import { HIDDEN, ALWAYS_HIDDEN } from "@aedart/contracts/support/concerns"; +import { AbstractClassError } from "@aedart/support/exceptions"; /** * Abstract Concern @@ -29,7 +30,7 @@ export default abstract class AbstractConcern implements Concern public constructor(owner: object) { if (new.target === AbstractConcern) { - throw new Error('Unable to make a new instance of abstract class'); + throw new AbstractClassError(AbstractConcern); } this.#concernOwner = owner; diff --git a/tests/browser/packages/support/concerns/AbstractConcern.test.js b/tests/browser/packages/support/concerns/AbstractConcern.test.js index f5a77de4..876b74a2 100644 --- a/tests/browser/packages/support/concerns/AbstractConcern.test.js +++ b/tests/browser/packages/support/concerns/AbstractConcern.test.js @@ -1,5 +1,6 @@ import { AbstractConcern } from "@aedart/support/concerns"; import { HIDDEN } from "@aedart/contracts/support/concerns"; +import { AbstractClassError } from "@aedart/support/exceptions"; describe('@aedart/support/concerns', () => { @@ -11,7 +12,7 @@ describe('@aedart/support/concerns', () => { } expect(callback) - .toThrowError(Error); + .toThrowError(AbstractClassError); }); it('can obtain concern owner instance', () => { From 23eba43f5ba6e67e4e347473e79f53be95bd96cf Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 13:53:57 +0100 Subject: [PATCH 090/424] Add Concern Exception interface --- .../concerns/exceptions/ConcernException.ts | 18 ++++++++++++++++++ .../src/support/concerns/exceptions/index.ts | 4 ++++ .../contracts/src/support/concerns/index.ts | 2 ++ 3 files changed, 24 insertions(+) create mode 100644 packages/contracts/src/support/concerns/exceptions/ConcernException.ts create mode 100644 packages/contracts/src/support/concerns/exceptions/index.ts diff --git a/packages/contracts/src/support/concerns/exceptions/ConcernException.ts b/packages/contracts/src/support/concerns/exceptions/ConcernException.ts new file mode 100644 index 00000000..a0f85fb3 --- /dev/null +++ b/packages/contracts/src/support/concerns/exceptions/ConcernException.ts @@ -0,0 +1,18 @@ +import { Throwable } from "@aedart/contracts/support/exceptions"; +import { Constructor } from "@aedart/contracts"; +import Concern from "../Concern"; + +/** + * Concern Exception + * + * To be thrown when a concern class is the cause of an error. + */ +export default interface ConcernException extends Throwable { + + /** + * The Concern class that caused this error or exception + * + * @type {Constructor|undefined} + */ + concern?: Constructor +} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/exceptions/index.ts b/packages/contracts/src/support/concerns/exceptions/index.ts new file mode 100644 index 00000000..0a4b9abd --- /dev/null +++ b/packages/contracts/src/support/concerns/exceptions/index.ts @@ -0,0 +1,4 @@ +import ConcernException from "./ConcernException"; +export { + type ConcernException +} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 26124f6d..69c06ae0 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -86,4 +86,6 @@ export { type Owner } +export * from './exceptions/index'; + export * from './types'; \ No newline at end of file From b790c056b9bb386bd9e16c658fef288d9d083bf3 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 14:18:42 +0100 Subject: [PATCH 091/424] Change concern property to be mandatory --- .../src/support/concerns/exceptions/ConcernException.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/exceptions/ConcernException.ts b/packages/contracts/src/support/concerns/exceptions/ConcernException.ts index a0f85fb3..f78269c6 100644 --- a/packages/contracts/src/support/concerns/exceptions/ConcernException.ts +++ b/packages/contracts/src/support/concerns/exceptions/ConcernException.ts @@ -12,7 +12,9 @@ export default interface ConcernException extends Throwable { /** * The Concern class that caused this error or exception * - * @type {Constructor|undefined} + * @readonly + * + * @type {Constructor} */ - concern?: Constructor + readonly concern: Constructor } \ No newline at end of file From ed8add3f176ebfe152e5447986bdfdac1986154f Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 14:31:58 +0100 Subject: [PATCH 092/424] Add boot and not registered exception interfaces --- .../src/support/concerns/exceptions/BootException.ts | 10 ++++++++++ .../concerns/exceptions/NotRegisteredException.ts | 11 +++++++++++ .../src/support/concerns/exceptions/index.ts | 6 +++++- 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 packages/contracts/src/support/concerns/exceptions/BootException.ts create mode 100644 packages/contracts/src/support/concerns/exceptions/NotRegisteredException.ts diff --git a/packages/contracts/src/support/concerns/exceptions/BootException.ts b/packages/contracts/src/support/concerns/exceptions/BootException.ts new file mode 100644 index 00000000..8304c0a9 --- /dev/null +++ b/packages/contracts/src/support/concerns/exceptions/BootException.ts @@ -0,0 +1,10 @@ +import ConcernException from './ConcernException' + +/** + * Concern Boot Exception + * + * To be thrown when a concern class is unable to boot. + * + * @see ConcernException + */ +export default interface BootException extends ConcernException {} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/exceptions/NotRegisteredException.ts b/packages/contracts/src/support/concerns/exceptions/NotRegisteredException.ts new file mode 100644 index 00000000..b1fb56d4 --- /dev/null +++ b/packages/contracts/src/support/concerns/exceptions/NotRegisteredException.ts @@ -0,0 +1,11 @@ +import ConcernException from './ConcernException'; + +/** + * Concern Not Registered Exception + * + * To be thrown when a given concern is expected registered in a [Container]{@link import('@aedart/contracts/support/concerns').Container}, + * but isn't. + * + * @see ConcernException + */ +export default interface NotRegisteredException extends ConcernException {} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/exceptions/index.ts b/packages/contracts/src/support/concerns/exceptions/index.ts index 0a4b9abd..be753a7a 100644 --- a/packages/contracts/src/support/concerns/exceptions/index.ts +++ b/packages/contracts/src/support/concerns/exceptions/index.ts @@ -1,4 +1,8 @@ +import BootException from "./BootException"; import ConcernException from "./ConcernException"; +import NotRegisteredException from "./NotRegisteredException"; export { - type ConcernException + type BootException, + type ConcernException, + type NotRegisteredException } \ No newline at end of file From d22e7eebd8d242fcdcbe5814f6eccc30ed07cb01 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 14:32:22 +0100 Subject: [PATCH 093/424] Change Container, throw concern related exceptions --- packages/contracts/src/support/concerns/Container.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index 8cabafb2..049689f4 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -46,7 +46,7 @@ export default interface Container * @return {Concern|null} Concern instance or `null` if provided concern class * is not registered in this container. * - * @throws {Error} + * @throws {ConcernException} */ get(concern: Constructor): T|null; @@ -68,8 +68,8 @@ export default interface Container * * @return {Concern} New concern instance * - * @throws {Error} If provided concern class is not registered in this container, - * or if concern was already booted. + * @throws {NotRegisteredException} If concern class is not registered in this container + * @throws {BootException} If concern is unable to be booted, e.g. if already booted */ boot(concern: Constructor): T; From ab9ddfd4fc5e84a5422779dd236d7c71d4358421 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 14:40:38 +0100 Subject: [PATCH 094/424] Add Concern related errors (implementation of concern exceptions) --- .../src/concerns/exceptions/BootError.ts | 30 +++++++++++ .../src/concerns/exceptions/ConcernError.ts | 50 +++++++++++++++++++ .../concerns/exceptions/NotRegisteredError.ts | 30 +++++++++++ .../support/src/concerns/exceptions/index.ts | 8 +++ packages/support/src/concerns/index.ts | 4 +- 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 packages/support/src/concerns/exceptions/BootError.ts create mode 100644 packages/support/src/concerns/exceptions/ConcernError.ts create mode 100644 packages/support/src/concerns/exceptions/NotRegisteredError.ts create mode 100644 packages/support/src/concerns/exceptions/index.ts diff --git a/packages/support/src/concerns/exceptions/BootError.ts b/packages/support/src/concerns/exceptions/BootError.ts new file mode 100644 index 00000000..8864f7d5 --- /dev/null +++ b/packages/support/src/concerns/exceptions/BootError.ts @@ -0,0 +1,30 @@ +import type { BootException, Concern } from "@aedart/contracts/support/concerns"; +import type { Constructor} from "@aedart/contracts"; +import ConcernError from "./ConcernError"; + +/** + * Concern Boot Error + * + * @see BootException + */ +export default class BootError extends ConcernError implements BootException +{ + /** + * Create a new Concern Boot Error instance + * + * @param {Constructor} concern + * @param {string} message + * @param {ErrorOptions} [options] + */ + constructor(concern: Constructor, message: string, options?: ErrorOptions) { + super(concern, message, options); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, BootError); + } else { + this.stack = (new Error()).stack; + } + + this.name = "BootError"; + } +} \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/ConcernError.ts b/packages/support/src/concerns/exceptions/ConcernError.ts new file mode 100644 index 00000000..832452bb --- /dev/null +++ b/packages/support/src/concerns/exceptions/ConcernError.ts @@ -0,0 +1,50 @@ +import type { ConcernException, Concern } from "@aedart/contracts/support/concerns"; +import type { Constructor } from "@aedart/contracts"; + +/** + * Concern Error + * + * @see ConcernException + */ +export default class ConcernError extends Error implements ConcernException +{ + /** + * The Concern class that caused this error or exception + * + * @private + * + * @type {Constructor|undefined} + */ + readonly #concern: Constructor + + /** + * Create a new Concern Error instance + * + * @param {Constructor} concern + * @param {string} message + * @param {ErrorOptions} [options] + */ + constructor(concern: Constructor, message: string, options?: ErrorOptions) { + super(message, options || { cause: { concern: concern } }); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ConcernError); + } else { + this.stack = (new Error()).stack; + } + + this.name = "ConcernError"; + this.#concern = concern; + } + + /** + * The Concern class that caused this error or exception + * + * @readonly + * + * @type {Constructor} + */ + get concern(): Constructor { + return this.#concern; + } +} \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/NotRegisteredError.ts b/packages/support/src/concerns/exceptions/NotRegisteredError.ts new file mode 100644 index 00000000..d21f303e --- /dev/null +++ b/packages/support/src/concerns/exceptions/NotRegisteredError.ts @@ -0,0 +1,30 @@ +import type { Concern, NotRegisteredException } from "@aedart/contracts/support/concerns"; +import type { Constructor } from "@aedart/contracts"; +import ConcernError from './ConcernError'; + +/** + * Concern Not Registered Error + * + * @see NotRegisteredException + */ +export default class NotRegisteredError extends ConcernError implements NotRegisteredException +{ + /** + * Create a new Concern Not Registered Error instance + * + * @param {Constructor} concern + * @param {string} message + * @param {ErrorOptions} [options] + */ + constructor(concern: Constructor, message: string, options?: ErrorOptions) { + super(concern, message, options); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, NotRegisteredError); + } else { + this.stack = (new Error()).stack; + } + + this.name = "NotRegisteredError"; + } +} \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/index.ts b/packages/support/src/concerns/exceptions/index.ts new file mode 100644 index 00000000..8ea12cda --- /dev/null +++ b/packages/support/src/concerns/exceptions/index.ts @@ -0,0 +1,8 @@ +import BootError from "./BootError"; +import ConcernError from "./ConcernError"; +import NotRegisteredError from "./NotRegisteredError"; +export { + BootError, + ConcernError, + NotRegisteredError +}; \ No newline at end of file diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index f60c1b4d..122ebcc5 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -2,4 +2,6 @@ import AbstractConcern from "./AbstractConcern"; export { AbstractConcern -}; \ No newline at end of file +}; + +export * from './exceptions/index'; \ No newline at end of file From f61413838a6c9870816accab862561a9348ec8f8 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 14:51:15 +0100 Subject: [PATCH 095/424] Change error options handling Create a default error options where the concern class is always part of the cause. --- packages/support/src/concerns/exceptions/ConcernError.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/support/src/concerns/exceptions/ConcernError.ts b/packages/support/src/concerns/exceptions/ConcernError.ts index 832452bb..4d13b2cf 100644 --- a/packages/support/src/concerns/exceptions/ConcernError.ts +++ b/packages/support/src/concerns/exceptions/ConcernError.ts @@ -25,7 +25,14 @@ export default class ConcernError extends Error implements ConcernException * @param {ErrorOptions} [options] */ constructor(concern: Constructor, message: string, options?: ErrorOptions) { - super(message, options || { cause: { concern: concern } }); + super( + message, + Object.assign( + Object.create(null), + options || {}, + { cause: { concern: concern } } + ) + ); if (Error.captureStackTrace) { Error.captureStackTrace(this, ConcernError); From 558efdb0cabcfaa60cd27e17f91b7114606ea9ae Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 14:58:34 +0100 Subject: [PATCH 096/424] Fix thrown exception for bootAll() --- packages/contracts/src/support/concerns/Container.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index 049689f4..fbe8621b 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -76,7 +76,7 @@ export default interface Container /** * Boots all registered concern classes * - * @throws {Error} + * @throws {ConcernException} */ bootAll(): void; From febc058f62e8f5c2dfa1e1118f7addf60ee997ae Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 15:08:46 +0100 Subject: [PATCH 097/424] Change constructor, remove message There is no reason this exception should allow a custom message. --- .../support/src/concerns/exceptions/NotRegisteredError.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/support/src/concerns/exceptions/NotRegisteredError.ts b/packages/support/src/concerns/exceptions/NotRegisteredError.ts index d21f303e..d5690edb 100644 --- a/packages/support/src/concerns/exceptions/NotRegisteredError.ts +++ b/packages/support/src/concerns/exceptions/NotRegisteredError.ts @@ -1,6 +1,7 @@ import type { Concern, NotRegisteredException } from "@aedart/contracts/support/concerns"; import type { Constructor } from "@aedart/contracts"; import ConcernError from './ConcernError'; +import { getNameOrDesc } from "@aedart/support/reflections"; /** * Concern Not Registered Error @@ -13,11 +14,10 @@ export default class NotRegisteredError extends ConcernError implements NotRegis * Create a new Concern Not Registered Error instance * * @param {Constructor} concern - * @param {string} message * @param {ErrorOptions} [options] */ - constructor(concern: Constructor, message: string, options?: ErrorOptions) { - super(concern, message, options); + constructor(concern: Constructor, options?: ErrorOptions) { + super(concern, `${getNameOrDesc(concern)} is not registered in concerns container`, options); if (Error.captureStackTrace) { Error.captureStackTrace(this, NotRegisteredError); From daac8473b9c25a87cb6f5eee5380764e52722239 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 15:13:15 +0100 Subject: [PATCH 098/424] Add Concerns Container --- .../support/src/concerns/ConcernsContainer.ts | 210 ++++++++++++++++++ packages/support/src/concerns/index.ts | 4 +- 2 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 packages/support/src/concerns/ConcernsContainer.ts diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts new file mode 100644 index 00000000..e5323d6b --- /dev/null +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -0,0 +1,210 @@ +import type { + Container, + Concern, + Owner +} from "@aedart/contracts/support/concerns"; +import type { Constructor } from "@aedart/contracts"; +import { getNameOrDesc } from "@aedart/support/reflections"; +import BootError from "./exceptions/BootError"; +import NotRegisteredError from "./exceptions/NotRegisteredError"; + +/** + * Concerns Container + * + * @see Container + */ +export default class ConcernsContainer implements Container +{ + /** + * Map that holds concern class constructors + * and actual concern instances + * + * @private + * @readonly + * + * @type {Map, Concern|undefined>} + */ + readonly #map: Map, Concern|undefined>; + + /** + * The concerns owner of this container + * + * @private + * @readonly + * + * @type {Owner} + */ + readonly #owner: Owner; + + /** + * Create a new Concerns Container instance + * + * @param {Owner} owner + * @param {Constructor[]} concerns + */ + public constructor(owner: Owner, concerns: Constructor[]) { + this.#owner = owner; + this.#map = new Map, Concern | undefined>(); + + for(const concern: Constructor of concerns) { + this.#map.set(concern, undefined); + } + } + + /** + * The amount of concerns in this container + * + * @readonly + * + * @type {number} + */ + public get size(): number + { + return this.#map.size; + } + + /** + * Get the concerns container owner + * + * @readonly + * + * @return {Owner} + */ + public get owner(): Owner + { + return this.#owner; + } + + /** + * Determine if concern class is registered in this container + * + * @param {Constructor} concern + * + * @return {boolean} + */ + public has(concern: Constructor): boolean + { + return this.#map.has(concern); + } + + /** + * Retrieve concern instance for given concern class + * + * **Note**: _If concern class is registered in this container, but not yet + * booted, then this method will boot it via the {@link boot} method, and return + * the resulting instance._ + * + * @template T extends {@link Concern} + * + * @param {Constructor} concern + * + * @return {Concern|null} Concern instance or `null` if provided concern class + * is not registered in this container. + * + * @throws {ConcernError} + */ + public get(concern: Constructor): T|null + { + if (!this.has(concern)) { + return null; + } + + if (!this.hasBooted(concern)) { + return this.boot(concern); + } + + return this.#map.get(concern) as T; + } + + /** + * Determine if concern class has been booted + * + * @param {Constructor} concern + * + * @return {boolean} + */ + public hasBooted(concern: Constructor): boolean + { + return this.has(concern) && this.#map.get(concern) !== undefined; + } + + /** + * Boot concern class + * + * @template T extends {@link Concern} + * + * @param {Constructor} concern + * + * @return {Concern} New concern instance + * + * @throws {NotRegisteredError} If concern class is not registered in this container + * @throws {BootError} If concern is unable to be booted, e.g. if already booted + */ + public boot(concern: Constructor): T + { + // Fail if given concern is not in this container + if (!this.has(concern)) { + throw new NotRegisteredError(concern, { cause: { owner: this.owner } }); + } + + // Fail if concern instance already exists (has booted) + let instance: T | undefined = this.#map.get(concern); + if (instance !== undefined) { + throw new BootError(concern, `${getNameOrDesc(concern)} is already booted`, { cause: { owner: this.owner } }); + } + + // Boot the concern (create new instance) and register it... + try { + instance = new concern(this.owner); + this.#map.set(concern, instance); + } catch (error) { + const reason: string = error?.message || 'unknown reason!'; + throw new BootError(concern, `Unable to boot ${getNameOrDesc(concern)}: ${reason}`, { cause: { previous: error, owner: this.owner } }); + } + + return instance; + } + + /** + * Boots all registered concern classes + * + * @throws {ConcernError} + */ + public bootAll(): void + { + const concerns: IterableIterator> = this.all(); + for (const concern: Constructor of concerns) { + this.boot(concern); + } + } + + /** + * Determine if this container is empty + * + * @return {boolean} + */ + public isEmpty(): boolean + { + return this.size === 0; + } + + /** + * Opposite of {@link isEmpty} + * + * @return {boolean} + */ + public isNotEmpty(): boolean + { + return !this.isEmpty(); + } + + /** + * Returns all concern classes + * + * @return {IterableIterator>} + */ + public all(): IterableIterator> + { + return this.#map.keys(); + } +} \ No newline at end of file diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 122ebcc5..8c7fe3d0 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -1,7 +1,9 @@ import AbstractConcern from "./AbstractConcern"; +import ConcernsContainer from "./ConcernsContainer"; export { - AbstractConcern + AbstractConcern, + ConcernsContainer }; export * from './exceptions/index'; \ No newline at end of file From 7df1bb432433258e6d9042bdc4569e09e4f9f6e0 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 16:00:58 +0100 Subject: [PATCH 099/424] Improve error messages --- packages/support/src/concerns/ConcernsContainer.ts | 4 ++-- .../support/src/concerns/exceptions/NotRegisteredError.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts index e5323d6b..e98da381 100644 --- a/packages/support/src/concerns/ConcernsContainer.ts +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -150,7 +150,7 @@ export default class ConcernsContainer implements Container // Fail if concern instance already exists (has booted) let instance: T | undefined = this.#map.get(concern); if (instance !== undefined) { - throw new BootError(concern, `${getNameOrDesc(concern)} is already booted`, { cause: { owner: this.owner } }); + throw new BootError(concern, `Concern ${getNameOrDesc(concern)} is already booted`, { cause: { owner: this.owner } }); } // Boot the concern (create new instance) and register it... @@ -159,7 +159,7 @@ export default class ConcernsContainer implements Container this.#map.set(concern, instance); } catch (error) { const reason: string = error?.message || 'unknown reason!'; - throw new BootError(concern, `Unable to boot ${getNameOrDesc(concern)}: ${reason}`, { cause: { previous: error, owner: this.owner } }); + throw new BootError(concern, `Unable to boot concern ${getNameOrDesc(concern)}: ${reason}`, { cause: { previous: error, owner: this.owner } }); } return instance; diff --git a/packages/support/src/concerns/exceptions/NotRegisteredError.ts b/packages/support/src/concerns/exceptions/NotRegisteredError.ts index d5690edb..7e70065b 100644 --- a/packages/support/src/concerns/exceptions/NotRegisteredError.ts +++ b/packages/support/src/concerns/exceptions/NotRegisteredError.ts @@ -17,7 +17,7 @@ export default class NotRegisteredError extends ConcernError implements NotRegis * @param {ErrorOptions} [options] */ constructor(concern: Constructor, options?: ErrorOptions) { - super(concern, `${getNameOrDesc(concern)} is not registered in concerns container`, options); + super(concern, `Concern ${getNameOrDesc(concern)} is not registered in concerns container`, options); if (Error.captureStackTrace) { Error.captureStackTrace(this, NotRegisteredError); From a714c888a63c977e920ca3c5edbfef82bc041318 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 16:20:06 +0100 Subject: [PATCH 100/424] Fix incorrect Error Options merging --- .../src/concerns/exceptions/ConcernError.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/support/src/concerns/exceptions/ConcernError.ts b/packages/support/src/concerns/exceptions/ConcernError.ts index 4d13b2cf..56a8becc 100644 --- a/packages/support/src/concerns/exceptions/ConcernError.ts +++ b/packages/support/src/concerns/exceptions/ConcernError.ts @@ -13,7 +13,7 @@ export default class ConcernError extends Error implements ConcernException * * @private * - * @type {Constructor|undefined} + * @type {Constructor} */ readonly #concern: Constructor @@ -25,14 +25,7 @@ export default class ConcernError extends Error implements ConcernException * @param {ErrorOptions} [options] */ constructor(concern: Constructor, message: string, options?: ErrorOptions) { - super( - message, - Object.assign( - Object.create(null), - options || {}, - { cause: { concern: concern } } - ) - ); + super(message, options || { cause: {} }); if (Error.captureStackTrace) { Error.captureStackTrace(this, ConcernError); @@ -42,6 +35,9 @@ export default class ConcernError extends Error implements ConcernException this.name = "ConcernError"; this.#concern = concern; + + // Force set the concern in the cause (in case custom was provided) + this.cause.concern = concern; } /** From 7230ffe64d3877b55a0b5dfc58a5195956eff273 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 16:41:12 +0100 Subject: [PATCH 101/424] Change get() method, throw exception if concern class not registered This should be much safer to use for developers and cause less confusion about when a concern is registered or not. --- packages/contracts/src/support/concerns/Container.ts | 6 +++--- packages/support/src/concerns/ConcernsContainer.ts | 10 +++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index fbe8621b..0f08fef8 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -43,12 +43,12 @@ export default interface Container * * @param {Constructor} concern * - * @return {Concern|null} Concern instance or `null` if provided concern class - * is not registered in this container. + * @return {Concern} The booted instance of the concern class. If concern class was + * previously booted, then that instance is returned. * * @throws {ConcernException} */ - get(concern: Constructor): T|null; + get(concern: Constructor): T; /** * Determine if concern class has been booted diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts index e98da381..274268a5 100644 --- a/packages/support/src/concerns/ConcernsContainer.ts +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -98,17 +98,13 @@ export default class ConcernsContainer implements Container * * @param {Constructor} concern * - * @return {Concern|null} Concern instance or `null` if provided concern class - * is not registered in this container. + * @return {Concern} The booted instance of the concern class. If concern class was + * previously booted, then that instance is returned. * * @throws {ConcernError} */ - public get(concern: Constructor): T|null + public get(concern: Constructor): T { - if (!this.has(concern)) { - return null; - } - if (!this.hasBooted(concern)) { return this.boot(concern); } From 57c038927b5eaa3b58ba258eb5bf605857fcb09f Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 16:43:15 +0100 Subject: [PATCH 102/424] Add tests for Concerns Container --- .../concerns/ConcernsContainer.test.js | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 tests/browser/packages/support/concerns/ConcernsContainer.test.js diff --git a/tests/browser/packages/support/concerns/ConcernsContainer.test.js b/tests/browser/packages/support/concerns/ConcernsContainer.test.js new file mode 100644 index 00000000..cd7a5241 --- /dev/null +++ b/tests/browser/packages/support/concerns/ConcernsContainer.test.js @@ -0,0 +1,302 @@ +import { + ConcernsContainer, + AbstractConcern, + NotRegisteredError, BootError +} from "@aedart/support/concerns"; +import { getNameOrDesc } from "@aedart/support/reflections"; + +describe('@aedart/support/concerns', () => { + describe('ConcernsContainer', () => { + + it('can create new instance', () => { + const owner = new class {}; + const container = new ConcernsContainer(owner, []); + + // ----------------------------------------------------------------------------------- // + + expect(container) + .toBeInstanceOf(ConcernsContainer); + + expect(container.size) + .withContext('No concerns should be available') + .toBe(0); + + expect(container.isEmpty()) + .withContext('Should be empty') + .toBeTrue(); + + expect(container.isNotEmpty()) + .withContext('Should be empty (via is not empty)') + .toBeFalse(); + + expect(container.owner) + .withContext('Incorrect owner') + .toBe(owner); + }); + + it('can create instance with concerns registered', () => { + + class A extends AbstractConcern {} + class B extends AbstractConcern {} + class C extends AbstractConcern {} + const concerns = [ A, B, C ]; + + const container = new ConcernsContainer(new class {}, concerns); + + // ----------------------------------------------------------------------------------- // + + expect(container.size) + .withContext('Incorrect size') + .toBe(concerns.length); + + expect(container.isEmpty()) + .withContext('Should NOT be empty') + .toBeFalse() + + expect(container.isNotEmpty()) + .withContext('Should NOT be empty (via is not empty)') + .toBeTrue() + }); + + it('can obtain all registered concern classes', () => { + + class A extends AbstractConcern {} + class B extends AbstractConcern {} + class C extends AbstractConcern {} + const concerns = [ A, B, C ]; + + const container = new ConcernsContainer(new class {}, concerns); + + // ----------------------------------------------------------------------------------- // + + const all = container.all(); + let c = 0; + for (const concern of all) { + expect(concerns.includes(concern)) + .withContext(`Unknown concern ${getNameOrDesc(concern)} in container`) + .toBeTrue(); + + c++; + } + + expect(c) + .withContext('Invalid amount of concern classes returned by all()') + .toBe(concerns.length) + }); + + it('can determine if concern class is registered', () => { + class A extends AbstractConcern {} + class B extends AbstractConcern {} + class C extends AbstractConcern {} + const concerns = [ A, B, C ]; + + const container = new ConcernsContainer(new class {}, concerns); + + // ----------------------------------------------------------------------------------- // + + // Determine if exists in container + for (const concern of concerns) { + expect(container.has(concern)) + .withContext(`${getNameOrDesc(concern)} SHOULD be in container`) + .toBeTrue(); + } + }); + + it('can boot concern', () => { + class A extends AbstractConcern {} + const owner = new class {}; + + const container = new ConcernsContainer(owner, [ A ]); + + // ----------------------------------------------------------------------------------- // + + expect(container.hasBooted(A)) + .withContext('Concern should not (yet) be booted') + .toBeFalse(); + + // ----------------------------------------------------------------------------------- // + + const concern = container.boot(A); + + expect(concern) + .withContext('Incorrect boot of concern class') + .toBeInstanceOf(A); + + expect(concern.concernOwner) + .withContext('Concern owner not set') + .toBe(owner); + + expect(container.hasBooted(A)) + .withContext('Concern SHOULD be booted') + .toBeTrue(); + }); + + it('fails booting concern if not registered', () => { + class A extends AbstractConcern {} + + const container = new ConcernsContainer(new class {}, []); + + // ----------------------------------------------------------------------------------- // + + const callback = () => { + return container.boot(A); + } + + // ----------------------------------------------------------------------------------- // + + expect(callback) + .toThrowError(NotRegisteredError); + }); + + it('fails booting concern if already booted', () => { + class A extends AbstractConcern {} + + const container = new ConcernsContainer(new class {}, [ A ]); + container.boot(A); + + // ----------------------------------------------------------------------------------- // + + const callback = () => { + // Concern should already be booted at this point and thus exception is expected + // to be thrown... + container.boot(A); + } + + // ----------------------------------------------------------------------------------- // + + expect(callback) + .toThrowError(BootError); + }); + + it('fails if concern throws exception when instantiated', () => { + const msg = 'Test'; + class A extends AbstractConcern { + constructor(owner) { + super(owner); + + throw new Error(msg); + } + } + + const container = new ConcernsContainer(new class {}, [ A ]); + + // ----------------------------------------------------------------------------------- // + + let wasThrown = false; + try { + container.boot(A); + } catch (error) { + wasThrown = true; + + // Debug + // console.log(error.toString()); + // console.log(error.cause); + + expect(error) + .withContext('Should be instance of BootError') + .toBeInstanceOf(BootError); + + expect(error?.cause?.previous) + .withContext('Custom error cause - previous - not set!') + .not + .toBeUndefined(); + + expect(error?.cause?.owner) + .withContext('Custom error cause - owner - not set!') + .not + .toBeUndefined(); + + expect(error?.cause?.concern) + .withContext('Custom error cause - concern - incorrect concern!') + .toBe(A); + } + + expect(wasThrown) + .withContext('Exception was expected thrown during concern initialisation') + .toBeTrue(); + }); + + it('can boot all concerns', () => { + + class A extends AbstractConcern {} + class B extends AbstractConcern {} + class C extends AbstractConcern {} + const concerns = [ A, B, C ]; + + const container = new ConcernsContainer(new class {}, concerns); + + // ----------------------------------------------------------------------------------- // + + container.bootAll(); + + // ----------------------------------------------------------------------------------- // + + for (const concern of concerns) { + expect(container.hasBooted(concern)) + .withContext(`${getNameOrDesc(concern)} not booted`) + .toBeTrue(); + } + }); + + it('boots concern when obtained via get()', () => { + class A extends AbstractConcern {} + const owner = new class {}; + + const container = new ConcernsContainer(owner, [ A ]); + + // ----------------------------------------------------------------------------------- // + + expect(container.hasBooted(A)) + .withContext('Concern should not (yet) be booted') + .toBeFalse(); + + // ----------------------------------------------------------------------------------- // + + const concern = container.get(A); + + expect(concern) + .withContext('Incorrect boot of concern class') + .toBeInstanceOf(A); + + expect(container.hasBooted(A)) + .withContext('Concern SHOULD be booted') + .toBeTrue(); + }); + + it('returns booted concern, if already booted, when obtained via get()', () => { + class A extends AbstractConcern {} + const container = new ConcernsContainer(new class {}, [ A ]); + + // ----------------------------------------------------------------------------------- // + + const previousBooted = container.boot(A); + + expect(container.hasBooted(A)) + .withContext('Concern SHOULD be booted') + .toBeTrue(); + + // ----------------------------------------------------------------------------------- // + + const concern = container.get(A); + expect(concern) + .withContext('Incorrect instance obtained') + .toBeInstanceOf(A); + + expect(concern) + .withContext('Concern instance SHOULD match previous booted instance') + .toBe(previousBooted); + }); + + it('fails obtaining concern instance via get(), if not registered', () => { + class A extends AbstractConcern {} + const container = new ConcernsContainer(new class {}, []); + + const callback = () => { + return container.get(A); + } + + expect(callback) + .toThrowError(NotRegisteredError); + }); + }); +}); \ No newline at end of file From 1e256e6dc7438a00f8ca13c333c3e742cc7f3e3b Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 17:20:23 +0100 Subject: [PATCH 103/424] Change container, add call(), set and get property proxy methods These are intended to simply forward method and property calls to the concern instance in question. --- .../src/support/concerns/Container.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index 0f08fef8..0108936b 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -100,4 +100,47 @@ export default interface Container * @return {IterableIterator>} */ all(): IterableIterator>; + + /** + * Invoke a method with given arguments in concern instance + * + * @param {Constructor} concern + * @param {PropertyKey} method + * @param {...unknown} [arguments] + * + * @return {unknown} + * + * @throws {ConcernException} + * @throws {Error} + */ + call( + concern: Constructor, + method: PropertyKey, + ...arguments: unknown + ): unknown; + + /** + * Set the value of given property in concern instance + * + * @param {Constructor} concern + * @param {PropertyKey} property + * @param {unknown} value + * + * @throws {ConcernException} + * @throws {Error} + */ + setProperty(concern: Constructor, property: PropertyKey, value: unknown): void; + + /** + * Get value of given property in concern instance + * + * @param {Constructor} concern + * @param {PropertyKey} property + * + * @return {unknown} + * + * @throws {ConcernException} + * @throws {Error} + */ + getProperty(concern: Constructor, property: PropertyKey): unknown; } \ No newline at end of file From ff977b5a5d796ddbda3e3b3ba8fe0075c6f6f53b Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 18:00:46 +0100 Subject: [PATCH 104/424] Fix RollupError: Error when using sourcemap for reporting an error The "arguments" as parameter name of a method really screwed up Rollup - with no reasonable error message in the console. This took a lot of time to find... puh! --- packages/contracts/src/support/concerns/Container.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index 0108936b..bda416e4 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -106,18 +106,14 @@ export default interface Container * * @param {Constructor} concern * @param {PropertyKey} method - * @param {...unknown} [arguments] + * @param {...unknown} [args] * * @return {unknown} * * @throws {ConcernException} * @throws {Error} */ - call( - concern: Constructor, - method: PropertyKey, - ...arguments: unknown - ): unknown; + call(concern: Constructor, method: PropertyKey, ...args: unknown): unknown; /** * Set the value of given property in concern instance From 2ff0f66a9ac8246137e4797e7fe17d648c624c88 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 18:02:18 +0100 Subject: [PATCH 105/424] Shorted import path --- packages/support/src/concerns/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 8c7fe3d0..f2f7f1e3 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -6,4 +6,4 @@ export { ConcernsContainer }; -export * from './exceptions/index'; \ No newline at end of file +export * from './exceptions'; \ No newline at end of file From d699d7561c9bfd9cd71d0a44cbca801b3fa89b7e Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 18:04:24 +0100 Subject: [PATCH 106/424] Implement call(), setProperty() and getProperty() --- .../support/src/concerns/ConcernsContainer.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts index 274268a5..6969858c 100644 --- a/packages/support/src/concerns/ConcernsContainer.ts +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -203,4 +203,52 @@ export default class ConcernsContainer implements Container { return this.#map.keys(); } + + /** + * Invoke a method with given arguments in concern instance + * + * @param {Constructor} concern + * @param {PropertyKey} method + * @param {...unknown} [args] + * + * @return {unknown} + * + * @throws {ConcernError} + * @throws {Error} + */ + public call(concern: Constructor, method: PropertyKey, ...args: unknown): unknown + { + return this.get(concern)[method](...args); + } + + /** + * Set the value of given property in concern instance + * + * @param {Constructor} concern + * @param {PropertyKey} property + * @param {unknown} value + * + * @throws {ConcernError} + * @throws {Error} + */ + public setProperty(concern: Constructor, property: PropertyKey, value: unknown): void + { + this.get(concern)[property] = value; + } + + /** + * Get value of given property in concern instance + * + * @param {Constructor} concern + * @param {PropertyKey} property + * + * @return {unknown} + * + * @throws {ConcernError} + * @throws {Error} + */ + public getProperty(concern: Constructor, property: PropertyKey): unknown + { + return this.get(concern)[property]; + } } \ No newline at end of file From dd10307e014e31fd7370e00eec89867797f5611f Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 18:12:51 +0100 Subject: [PATCH 107/424] Add tests for call method, set and get property --- .../concerns/ConcernsContainer.test.js | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/browser/packages/support/concerns/ConcernsContainer.test.js b/tests/browser/packages/support/concerns/ConcernsContainer.test.js index cd7a5241..679aa7f8 100644 --- a/tests/browser/packages/support/concerns/ConcernsContainer.test.js +++ b/tests/browser/packages/support/concerns/ConcernsContainer.test.js @@ -298,5 +298,88 @@ describe('@aedart/support/concerns', () => { expect(callback) .toThrowError(NotRegisteredError); }); + + it('can call method in concern instance', () => { + + let wasInvoked = false; + const values = [ 1, 2, 3 ]; + + class A extends AbstractConcern { + sum(...args) { + wasInvoked = true; + + return args.reduce((x, currentValue) => { + return x + currentValue + }, 0); + } + } + const container = new ConcernsContainer(new class {}, [ A ]); + + // ----------------------------------------------------------------------------------- // + + const result = container.call(A, 'sum', ...values); + const expected = 6; + + expect(wasInvoked) + .withContext('Method was not called') + .toBeTrue(); + + expect(result) + .withContext('Incorrect method return value') + .toEqual(expected); + }); + + it('can call method without return in concern instance', () => { + + let wasInvoked = false; + class A extends AbstractConcern { + foo() { + wasInvoked = true; + } + } + const container = new ConcernsContainer(new class {}, [ A ]); + + // ----------------------------------------------------------------------------------- // + + const result = container.call(A, 'foo'); + + expect(wasInvoked) + .withContext('Method was not called') + .toBeTrue(); + + expect(result) + .withContext('No return value was expected') + .toBeUndefined(); + }); + + it('can set and get property value in concern instance', () => { + + class A extends AbstractConcern { + + #msg = null; + + set message(value) + { + this.#msg = value; + } + + get message() + { + return this.#msg + } + } + const container = new ConcernsContainer(new class {}, [ A ]); + + // ----------------------------------------------------------------------------------- // + + const message = 'Hallo World'; + + container.setProperty(A, 'message', message); + const result = container.getProperty(A, 'message'); + + expect(result) + .withContext('Property either not set or incorrect value returned') + .toBe(message); + }); }); }); \ No newline at end of file From 49f22dc7e8980db9ef78c1358ed201e2db2345d5 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 18:28:34 +0100 Subject: [PATCH 108/424] Change concern owner to default to "this" To allow concern instances to be as flexible ans easy to test as possible, the concern owner should just default to itself, when no owner is set. This should make it much easier to test a concern class in isolation. --- .../contracts/src/support/concerns/Concern.ts | 7 +++++-- .../support/src/concerns/AbstractConcern.ts | 21 +++++++++++++------ .../support/concerns/AbstractConcern.test.js | 11 ++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/contracts/src/support/concerns/Concern.ts b/packages/contracts/src/support/concerns/Concern.ts index 99c9a907..ecf447ec 100644 --- a/packages/contracts/src/support/concerns/Concern.ts +++ b/packages/contracts/src/support/concerns/Concern.ts @@ -9,9 +9,12 @@ export default interface Concern { /** - * Returns the target class instance this concern is injected into + * The owner class instance this concern is injected into, + * or `this` concern instance if no owner was set. + * + * @readonly * - * @return {object} + * @type {object} */ get concernOwner(): object; } \ No newline at end of file diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index 9f87a9f9..23a4bef4 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -12,9 +12,12 @@ import { AbstractClassError } from "@aedart/support/exceptions"; export default abstract class AbstractConcern implements Concern { /** - * The target class instance this concern is injected into + * The owner class instance this concern is injected into, + * or `this` concern instance. * + * @readonly * @private + * * @type {object} */ readonly #concernOwner: object; @@ -22,24 +25,30 @@ export default abstract class AbstractConcern implements Concern /** * Creates a new concern instance * - * @param {object} owner The target class instance this concern is injected into + * @param {object} [owner] The owner class instance this concern is injected into. + * Defaults to `this` concern instance if none given. * * @throws {Error} When concern is unable to preform initialisation, e.g. caused * by the owner or other circumstances. */ - public constructor(owner: object) + public constructor(owner?: object) { if (new.target === AbstractConcern) { throw new AbstractClassError(AbstractConcern); } - this.#concernOwner = owner; + this.#concernOwner = owner || this; } /** - * @inheritdoc + * The owner class instance this concern is injected into, + * or `this` concern instance if no owner was set. + * + * @readonly + * + * @type {object} */ - get concernOwner(): object + public get concernOwner(): object { return this.#concernOwner; } diff --git a/tests/browser/packages/support/concerns/AbstractConcern.test.js b/tests/browser/packages/support/concerns/AbstractConcern.test.js index 876b74a2..1adeeee1 100644 --- a/tests/browser/packages/support/concerns/AbstractConcern.test.js +++ b/tests/browser/packages/support/concerns/AbstractConcern.test.js @@ -29,6 +29,17 @@ describe('@aedart/support/concerns', () => { .toBe(owner); }); + it('defaults to concern instance when no owner given', () => { + + class MyConcern extends AbstractConcern {} + + const concern = new MyConcern(); + + const result = concern.concernOwner; + expect(result) + .toBe(concern); + }); + it('returns default list of hidden properties and methods', () => { class MyConcern extends AbstractConcern {} From 4b55238631c40b608531d98ede34dfc4dfcb79f3 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 18:28:43 +0100 Subject: [PATCH 109/424] Fix JSDoc --- packages/contracts/src/support/concerns/Container.ts | 2 +- packages/support/src/concerns/ConcernsContainer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index bda416e4..cf781824 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -19,7 +19,7 @@ export default interface Container /** * Get the concerns container owner * - * @return {Owner} + * @type {Owner} */ get owner(): Owner; diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts index 6969858c..6084277c 100644 --- a/packages/support/src/concerns/ConcernsContainer.ts +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -68,7 +68,7 @@ export default class ConcernsContainer implements Container * * @readonly * - * @return {Owner} + * @type {Owner} */ public get owner(): Owner { From a9f05ea18c607559a387c2381312d9db99c17062 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 16 Feb 2024 18:32:56 +0100 Subject: [PATCH 110/424] Add and additional assert Just to be sure that concern instance is not lost. --- .../packages/support/concerns/ConcernsContainer.test.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/browser/packages/support/concerns/ConcernsContainer.test.js b/tests/browser/packages/support/concerns/ConcernsContainer.test.js index 679aa7f8..3c4f36d7 100644 --- a/tests/browser/packages/support/concerns/ConcernsContainer.test.js +++ b/tests/browser/packages/support/concerns/ConcernsContainer.test.js @@ -380,6 +380,14 @@ describe('@aedart/support/concerns', () => { expect(result) .withContext('Property either not set or incorrect value returned') .toBe(message); + + // ----------------------------------------------------------------------------------- // + + const instance = container.get(A); + + expect(instance.message) + .withContext('Fatal: concern instance has changed, property value no longer retained') + .toEqual(message); }); }); }); \ No newline at end of file From e8cc1e667500d43d15f7f91a1d64a922fba1268a Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 11:12:11 +0100 Subject: [PATCH 111/424] Improve type IDE complained about this... --- packages/support/src/concerns/ConcernsContainer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts index 6084277c..84bc5fb3 100644 --- a/packages/support/src/concerns/ConcernsContainer.ts +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -144,7 +144,7 @@ export default class ConcernsContainer implements Container } // Fail if concern instance already exists (has booted) - let instance: T | undefined = this.#map.get(concern); + let instance: T | undefined = this.#map.get(concern) as T | undefined; if (instance !== undefined) { throw new BootError(concern, `Concern ${getNameOrDesc(concern)} is already booted`, { cause: { owner: this.owner } }); } From 171bdd2e66f485f22192b09be9ac27ac7e6472de Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 11:28:24 +0100 Subject: [PATCH 112/424] Improve description --- packages/contracts/src/support/concerns/Owner.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contracts/src/support/concerns/Owner.ts b/packages/contracts/src/support/concerns/Owner.ts index 39559019..76d915f2 100644 --- a/packages/contracts/src/support/concerns/Owner.ts +++ b/packages/contracts/src/support/concerns/Owner.ts @@ -3,6 +3,8 @@ import { CONCERNS } from "./index"; /** * Concerns Owner + * + * An owner is an object, e.g. instance of a class, that offers a concerns {@link Container}. */ export default interface Owner { From 5597fda17f273e21102e41defebde804e718db97 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 11:59:19 +0100 Subject: [PATCH 113/424] Add Concern Class / Owner Class pair type(s) --- .../contracts/src/support/concerns/types.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/types.ts b/packages/contracts/src/support/concerns/types.ts index d0dcaf2b..05618e39 100644 --- a/packages/contracts/src/support/concerns/types.ts +++ b/packages/contracts/src/support/concerns/types.ts @@ -1,3 +1,4 @@ +import { Constructor, ConstructorOrAbstractConstructor } from "@aedart/contracts"; import Concern from "./Concern"; /** @@ -12,4 +13,20 @@ export type Alias = PropertyKey; * class' prototype and acts as a proxy to the original property or method inside the * concern class instance. */ -export type Aliases = Record; \ No newline at end of file +export type Aliases = Record; + +/** + * Array that holds a {@link Concern} class / Owner class pair. + */ +export type ConcernOwnerClassPair = [ + Constructor, // Concern Class + ConstructorOrAbstractConstructor // Owner class that must use the concern class +]; + +/** + * A list of concern classes and their owner class in which they are + * used. + * + * @see ConcernOwnerClassPair + */ +export type ConcernClasses = ConcernOwnerClassPair[]; \ No newline at end of file From 3a16bc9bfc1b264559d25aa8a13dd635e87e7bd1 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 12:15:35 +0100 Subject: [PATCH 114/424] Add Must Use Concerns interface --- .../src/support/concerns/MustUseConcerns.ts | 34 +++++++++++++++++++ .../contracts/src/support/concerns/index.ts | 12 +++++++ 2 files changed, 46 insertions(+) create mode 100644 packages/contracts/src/support/concerns/MustUseConcerns.ts diff --git a/packages/contracts/src/support/concerns/MustUseConcerns.ts b/packages/contracts/src/support/concerns/MustUseConcerns.ts new file mode 100644 index 00000000..d96e9a3a --- /dev/null +++ b/packages/contracts/src/support/concerns/MustUseConcerns.ts @@ -0,0 +1,34 @@ +import type { ConcernClasses } from "./index"; +import { CONCERN_CLASSES } from "./index"; +import Owner from "./Owner"; + +/** + * Must Use Concerns + * + * Defines a list of concern classes that this class instance must use. + * + * **Note**: _The herein defined properties and methods MUST be implemented as static_ + */ +export default interface MustUseConcerns +{ + /** + * Constructor + * + * @param {...any} [args] + * + * @returns {Owner} + */ + new( + ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): Owner; + + /** + * Returns the concern classes that this class must use. + * + * **Note**: _If this class' parent class also must use concern classes, + * then those concern classes are included in the resulting list, ordered first!_ + * + * @return {ConcernClasses} + */ + [CONCERN_CLASSES](): ConcernClasses; +} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 69c06ae0..c4a9a5a1 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -28,6 +28,16 @@ export const SUPPORT_CONCERNS: unique symbol = Symbol('@aedart/contracts/support */ export const HIDDEN: unique symbol = Symbol('hidden'); +/** + * Symbol used to define a list of the concern classes that a given target class + * must use. + * + * @see {MustUseConcerns} + * + * @type {Symbol} + */ +export const CONCERN_CLASSES: unique symbol = Symbol('concern_classes'); + /** * Symbol used to define a "concerns container" property inside a target class' prototype * @@ -76,12 +86,14 @@ export const ALWAYS_HIDDEN: ReadonlyArray = [ import Concern from "./Concern"; import Configuration from "./Configuration"; import Container from "./Container"; +import MustUseConcerns from "./MustUseConcerns"; import Injector from "./Injector"; import Owner from "./Owner"; export { type Concern, type Configuration, type Container, + type MustUseConcerns, type Injector, type Owner } From c397ebc539ff62266b622ea027c580aeaa2c0051 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 12:31:04 +0100 Subject: [PATCH 115/424] Improve constructor return type --- .../contracts/src/support/concerns/MustUseConcerns.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/support/concerns/MustUseConcerns.ts b/packages/contracts/src/support/concerns/MustUseConcerns.ts index d96e9a3a..f4a72693 100644 --- a/packages/contracts/src/support/concerns/MustUseConcerns.ts +++ b/packages/contracts/src/support/concerns/MustUseConcerns.ts @@ -1,3 +1,4 @@ +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import type { ConcernClasses } from "./index"; import { CONCERN_CLASSES } from "./index"; import Owner from "./Owner"; @@ -8,19 +9,23 @@ import Owner from "./Owner"; * Defines a list of concern classes that this class instance must use. * * **Note**: _The herein defined properties and methods MUST be implemented as static_ + * + * @template T = object */ -export default interface MustUseConcerns +export default interface MustUseConcerns { /** * Constructor * + * @template T = object + * * @param {...any} [args] * - * @returns {Owner} + * @returns {ConstructorOrAbstractConstructor & Owner} */ new( ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ - ): Owner; + ): ConstructorOrAbstractConstructor & Owner; /** * Returns the concern classes that this class must use. From 64e3dbb6c049c2f8f74a9e52ae00aaa9761c9a18 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 12:40:38 +0100 Subject: [PATCH 116/424] Fix constructor return type --- packages/contracts/src/support/concerns/MustUseConcerns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/MustUseConcerns.ts b/packages/contracts/src/support/concerns/MustUseConcerns.ts index f4a72693..7c76c9ad 100644 --- a/packages/contracts/src/support/concerns/MustUseConcerns.ts +++ b/packages/contracts/src/support/concerns/MustUseConcerns.ts @@ -25,7 +25,7 @@ export default interface MustUseConcerns */ new( ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ - ): ConstructorOrAbstractConstructor & Owner; + ): ConstructorOrAbstractConstructor; /** * Returns the concern classes that this class must use. From 85a9727404fb796754188c34ff02b82e9337c3d3 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 12:47:22 +0100 Subject: [PATCH 117/424] Add Injection exception interface --- .../concerns/exceptions/InjectionException.ts | 20 +++++++++++++++++++ .../src/support/concerns/exceptions/index.ts | 2 ++ 2 files changed, 22 insertions(+) create mode 100644 packages/contracts/src/support/concerns/exceptions/InjectionException.ts diff --git a/packages/contracts/src/support/concerns/exceptions/InjectionException.ts b/packages/contracts/src/support/concerns/exceptions/InjectionException.ts new file mode 100644 index 00000000..e31e2dda --- /dev/null +++ b/packages/contracts/src/support/concerns/exceptions/InjectionException.ts @@ -0,0 +1,20 @@ +import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import ConcernException from "./ConcernException"; +import MustUseConcerns from "../MustUseConcerns"; + +/** + * Concern Injection Exception + * + * To be thrown when a concern class cannot be injected into a target class. + */ +export default interface InjectionException extends ConcernException +{ + /** + * The target class + * + * @readonly + * + * @type {ConstructorOrAbstractConstructor|MustUseConcerns} + */ + readonly target: ConstructorOrAbstractConstructor | MustUseConcerns; +} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/exceptions/index.ts b/packages/contracts/src/support/concerns/exceptions/index.ts index be753a7a..68cede44 100644 --- a/packages/contracts/src/support/concerns/exceptions/index.ts +++ b/packages/contracts/src/support/concerns/exceptions/index.ts @@ -1,8 +1,10 @@ import BootException from "./BootException"; import ConcernException from "./ConcernException"; +import InjectionException from "./InjectionException"; import NotRegisteredException from "./NotRegisteredException"; export { type BootException, type ConcernException, + type InjectionException, type NotRegisteredException } \ No newline at end of file From 143bb411d1dc3ff0e4d0dff4ab8d0efb092d45f8 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 13:41:56 +0100 Subject: [PATCH 118/424] Change the Injector's API The interface has become much more "implementation" oriented, but that's perhaps not a bad thing in this context. Actual implementation will show if the interface must be adapted to become more loosely defined - or perhaps even more strict?... --- .../src/support/concerns/Injector.ts | 123 ++++++++++-------- 1 file changed, 72 insertions(+), 51 deletions(-) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index 6381733e..bd0525fb 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -1,7 +1,7 @@ -import { Constructor, ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { Constructor } from "@aedart/contracts"; import Concern from "./Concern"; import Configuration from "./Configuration"; -import Container from "./Container"; +import MustUseConcerns from "./MustUseConcerns"; /** * Concerns Injector @@ -14,85 +14,106 @@ import Container from "./Container"; export default interface Injector { /** - * Inject one or more concerns into the target class and return the modified target. + * Injects concern classes into the target class and return the modified target. * - * **Note**: _This method performs the following (in given order):_ + * **Note**: _Method performs injection in the following way:_ * - * _**A**: Defines a new concerns {@link Container} in target class' prototype, via {@link defineContainer}._ + * _**A**: Defines the concern classes in target class, via {@link defineConcerns}._ * - * _**B**: Defines aliases (proxy properties and methods) in target class' prototype, via {@link defineAliases}._ + * _**B**: Defines a concerns container in target class' prototype, via {@link defineContainer}._ * - * @template T extends {@link ConstructorOrAbstractConstructor} = object - * @template C = {@link Concern} + * _**C**: Defines "aliases" (proxy properties and methods) in target class' prototype, via {@link defineAliases}._ * - * @param {T} target The target class concerns must be injected into - * @param {...Constructor|Configuration} concerns + * @template T = object + * @template C = {@link Concern} * - * @return {T} Given target class with concern classes injected into its prototype + * @param {T} target The target class that concerns classes must be injected into + * @param {Constructor | Configuration} concerns List of concern classes / injection configurations * - * @throws {TypeError|Error} + * @returns {MustUseConcerns} The modified target class + * + * @throws {InjectionException} */ inject< - T extends ConstructorOrAbstractConstructor = object, + T = object, C = Concern - >(target: T, ...concerns: Constructor|Configuration): T; + >(target: T, ...concerns: (Constructor|Configuration)[]): MustUseConcerns; /** - * Defines a concerns {@link Container} in target class' prototype using - * [CONCERNS]{@link import('@aedart/contracts/support/concerns').CONCERNS} as its property key. + * Defines the concern classes that must be used by the target class. + * + * **Note**: _Method changes the target class, such that it implements and respects the + * {@link MustUseConcerns} interface. The original target class' constructor remains the untouched!_ + * + * @template T = object * - * **Note**: _If target class has a parent that already has a container defined, then the - * concern classes it contains will be merged with those provided as argument for this method, - * and populated into the new container._ + * @param {T} target The target class that must define the concern classes to be used + * @param {Constructor[]} concerns List of concern classes + * + * @returns {MustUseConcerns} The modified target class + * + * @throws {InjectionException} If given concern classes conflict with target class' parent concern classes, + * e.g. in case of duplicates. Or, if unable to modify target class. + */ + defineConcerns(target: T, concerns: Constructor[]): MustUseConcerns; + + /** + * Defines a concerns {@link Container} in target class' prototype. * - * @param {object} target The target class in which a concerns container must be defined - * @param {Constructor[]} concerns The concern classes to populate the container with. + * **Note**: _Method changes the target class, such that it implements and respects the + * [Owner]{@link import('@aedart/contracts/support/concerns').Owner} interface!_ * - * @return {Container} + * @template T = object * - * @throws {TypeError} If duplicate concern classes are provided, or if provided concern classes are already - * defined in target class' parent. - * @throws {Error} If unable to define concerns container in target class. + * @param {MustUseConcerns} target The target in which a concerns container must be defined + * + * @returns {MustUseConcerns} The modified target class + * + * @throws {InjectionException} If unable to define concerns container in target class */ - defineContainer(target: object, concerns: Constructor[]): Container; - + defineContainer(target: MustUseConcerns): MustUseConcerns; + /** - * Create aliases (proxy properties and methods) in target class' prototype, to the properties - * or methods in given concerns. + * Defines "aliases" (proxy properties and methods) in target class' prototype, such that they + * point to the properties and methods available in the concern classes. + * + * **Note**: _Method defines each alias using the {@link defineAlias} method!_ * - * **Note**: _Method creates each alias using the {@link defineAlias} method!_ + * @template T = object * - * @param {object} target The target class aliases must be created in + * @param {MustUseConcerns} target The target in which "aliases" must be defined in * @param {Configuration[]} configurations List of concern injection configurations + * + * @returns {MustUseConcerns} The modified target class * - * @throws {TypeError} In case of conflicting aliases. - * @throws {Error} If unable to obtain keys from source concern or failure occurs when defining - * proxy properties or methods in target class' prototype. + * @throws {InjectionException} If case of alias naming conflicts. Or, if unable to define aliases in target class. */ - defineAliases(target: object, configurations: Configuration[]): void; + defineAliases(target: MustUseConcerns, configurations: Configuration[]): MustUseConcerns; /** - * Create an alias (a proxy) in target class' prototype, to a property or method in given concern. + * Defines an "alias" (proxy property or method) in target class' prototype, to a property or method + * in given concern. * - * **Note**: _Method will do nothing if a property or method already exists in the target, with the same - * name as the given alias!_ + * **Note**: _Method will do nothing, if a property or method already exists in the target class' prototype + * chain, with the same name as given "alias"._ * - * @param {object} target The target class the alias must be created in - * @param {PropertyKey} key Name of the property or method in the source concern class to create an alias for - * @param {PropertyKey} alias Alias for the key to create in the target class (the proxy property or method) - * @param {Constructor} source The concern to that the alias property or method must proxy to - * - * @return {boolean} True if a proxy property or method was created in target class. - * False if not, e.g. property or method with same name or symbol as the alias - * already exists in target. + * @template T = object + * + * @param {MustUseConcerns} target The target in which "alias" must be defined in + * @param {PropertyKey} alias Name of the "alias" in the target class (name of the proxy property or method) + * @param {PropertyKey} key Name of the property or method that the "alias" is for, in the concern class (`source`) + * @param {Constructor} source The concern class that holds the property or methods (`key`) * - * @throws {Error} If unable to obtain key from source concern or failure occurs when defining - * proxy property or method in target class' prototype. + * @returns {boolean} `true` if "alias" was in target class. `false` if not, e.g. a property or method already + * exists in target class' prototype chain, with the same name as the alias. + * + * @throws {InjectionException} If unable to define "alias" in target class, e.g. due to failure when obtaining + * or defining [property descriptors]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#description}. */ - defineAlias( - target: object, - key: PropertyKey, + defineAlias( + target: MustUseConcerns, alias: PropertyKey, + key: PropertyKey, source: Constructor ): boolean; } \ No newline at end of file From 942d61538d13b41c168ad25a27774b70a3b55d91 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 14:49:35 +0100 Subject: [PATCH 119/424] Change the inject method remove target Injector implementation is not going to be static, nor a singleton. Thus, it makes no sense to keep the target in the inject() method. --- .../contracts/src/support/concerns/Injector.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index bd0525fb..6edf2282 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -9,9 +9,11 @@ import MustUseConcerns from "./MustUseConcerns"; * Able to inject concerns into a target class and create alias (proxy) properties * and methods to the provided concerns' properties and methods, in the target class. * + * @template T = object The target class that concern classes must be injected into + * * @see {Concern} */ -export default interface Injector +export default interface Injector { /** * Injects concern classes into the target class and return the modified target. @@ -23,21 +25,17 @@ export default interface Injector * _**B**: Defines a concerns container in target class' prototype, via {@link defineContainer}._ * * _**C**: Defines "aliases" (proxy properties and methods) in target class' prototype, via {@link defineAliases}._ - * - * @template T = object + * + * @template T = object The target class that concern classes must be injected into * @template C = {@link Concern} - * - * @param {T} target The target class that concerns classes must be injected into + * * @param {Constructor | Configuration} concerns List of concern classes / injection configurations * * @returns {MustUseConcerns} The modified target class * * @throws {InjectionException} */ - inject< - T = object, - C = Concern - >(target: T, ...concerns: (Constructor|Configuration)[]): MustUseConcerns; + inject(...concerns: (Constructor|Configuration)[]): MustUseConcerns; /** * Defines the concern classes that must be used by the target class. From 74c972344567cebd125cc8012931c47cdcd6584f Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 14:52:11 +0100 Subject: [PATCH 120/424] Add getter for target class --- packages/contracts/src/support/concerns/Injector.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index 6edf2282..8b462c5d 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -15,6 +15,13 @@ import MustUseConcerns from "./MustUseConcerns"; */ export default interface Injector { + /** + * The target class + * + * @returns {T} + */ + get target(): T; + /** * Injects concern classes into the target class and return the modified target. * From 3d2d52cb046ad2fff79a0edd0258c9ee53422bb1 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 14:57:24 +0100 Subject: [PATCH 121/424] Fix style --- packages/support/src/concerns/exceptions/BootError.ts | 3 ++- packages/support/src/concerns/exceptions/ConcernError.ts | 6 ++++-- .../support/src/concerns/exceptions/NotRegisteredError.ts | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/support/src/concerns/exceptions/BootError.ts b/packages/support/src/concerns/exceptions/BootError.ts index 8864f7d5..68b7f6a8 100644 --- a/packages/support/src/concerns/exceptions/BootError.ts +++ b/packages/support/src/concerns/exceptions/BootError.ts @@ -16,7 +16,8 @@ export default class BootError extends ConcernError implements BootException * @param {string} message * @param {ErrorOptions} [options] */ - constructor(concern: Constructor, message: string, options?: ErrorOptions) { + constructor(concern: Constructor, message: string, options?: ErrorOptions) + { super(concern, message, options); if (Error.captureStackTrace) { diff --git a/packages/support/src/concerns/exceptions/ConcernError.ts b/packages/support/src/concerns/exceptions/ConcernError.ts index 56a8becc..8a05ae6f 100644 --- a/packages/support/src/concerns/exceptions/ConcernError.ts +++ b/packages/support/src/concerns/exceptions/ConcernError.ts @@ -24,7 +24,8 @@ export default class ConcernError extends Error implements ConcernException * @param {string} message * @param {ErrorOptions} [options] */ - constructor(concern: Constructor, message: string, options?: ErrorOptions) { + constructor(concern: Constructor, message: string, options?: ErrorOptions) + { super(message, options || { cause: {} }); if (Error.captureStackTrace) { @@ -47,7 +48,8 @@ export default class ConcernError extends Error implements ConcernException * * @type {Constructor} */ - get concern(): Constructor { + get concern(): Constructor + { return this.#concern; } } \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/NotRegisteredError.ts b/packages/support/src/concerns/exceptions/NotRegisteredError.ts index 7e70065b..5882d298 100644 --- a/packages/support/src/concerns/exceptions/NotRegisteredError.ts +++ b/packages/support/src/concerns/exceptions/NotRegisteredError.ts @@ -16,7 +16,8 @@ export default class NotRegisteredError extends ConcernError implements NotRegis * @param {Constructor} concern * @param {ErrorOptions} [options] */ - constructor(concern: Constructor, options?: ErrorOptions) { + constructor(concern: Constructor, options?: ErrorOptions) + { super(concern, `Concern ${getNameOrDesc(concern)} is not registered in concerns container`, options); if (Error.captureStackTrace) { From ada150abb0f7575e9f00b0c9bc7333ccf6714d43 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 15:05:35 +0100 Subject: [PATCH 122/424] Add Injection Error --- .../src/concerns/exceptions/InjectionError.ts | 61 +++++++++++++++++++ .../support/src/concerns/exceptions/index.ts | 2 + 2 files changed, 63 insertions(+) create mode 100644 packages/support/src/concerns/exceptions/InjectionError.ts diff --git a/packages/support/src/concerns/exceptions/InjectionError.ts b/packages/support/src/concerns/exceptions/InjectionError.ts new file mode 100644 index 00000000..9b145b9e --- /dev/null +++ b/packages/support/src/concerns/exceptions/InjectionError.ts @@ -0,0 +1,61 @@ +import ConcernError from "./ConcernError"; +import { Concern, InjectionException, MustUseConcerns } from "@aedart/contracts/support/concerns"; +import type { Constructor, ConstructorOrAbstractConstructor } from "@aedart/contracts"; + +/** + * Injection Error + * + * @see InjectionException + */ +export default class InjectionError extends ConcernError implements InjectionException +{ + /** + * The target class + * + * @readonly + * + * @type {ConstructorOrAbstractConstructor|MustUseConcerns} + */ + readonly #target: ConstructorOrAbstractConstructor | MustUseConcerns; + + /** + * Create a new Injection Error instance + * + * @param {ConstructorOrAbstractConstructor | MustUseConcerns} target + * @param {Constructor} concern + * @param {string} message + * @param {ErrorOptions} [options] + */ + constructor( + target: ConstructorOrAbstractConstructor | MustUseConcerns, + concern: Constructor, + message: string, + options?: ErrorOptions + ) { + super(concern, message, options); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ConcernError); + } else { + this.stack = (new Error()).stack; + } + + this.name = "InjectionError"; + this.#target = target; + + // Force set the target in the cause + this.cause.target = target; + } + + /** + * The target class + * + * @readonly + * + * @returns {ConstructorOrAbstractConstructor | MustUseConcerns} + */ + get target(): ConstructorOrAbstractConstructor | MustUseConcerns + { + return this.#target; + } +} \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/index.ts b/packages/support/src/concerns/exceptions/index.ts index 8ea12cda..78922f27 100644 --- a/packages/support/src/concerns/exceptions/index.ts +++ b/packages/support/src/concerns/exceptions/index.ts @@ -1,8 +1,10 @@ import BootError from "./BootError"; import ConcernError from "./ConcernError"; +import InjectionError from "./InjectionError"; import NotRegisteredError from "./NotRegisteredError"; export { BootError, ConcernError, + InjectionError, NotRegisteredError }; \ No newline at end of file From 94430dcc3bd695a6cf0f505cc49373e24bcdb433 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 15:15:08 +0100 Subject: [PATCH 123/424] Add Concerns Injector (incomplete) --- .../support/src/concerns/ConcernsInjector.ts | 174 ++++++++++++++++++ packages/support/src/concerns/index.ts | 4 +- 2 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 packages/support/src/concerns/ConcernsInjector.ts diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts new file mode 100644 index 00000000..2d050da1 --- /dev/null +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -0,0 +1,174 @@ +import { + Concern, + Injector, + MustUseConcerns, + Configuration +} from "@aedart/contracts/support/concerns"; +import type { Constructor } from "@aedart/contracts"; + +/** + * Concerns Injector + * + * @see Injector + */ +export default class ConcernsInjector implements Injector +{ + /** + * The target class + * + * @template T = object + * @type {T} + * + * @private + */ + readonly #target: T; + + /** + * Create a new Concerns Injector instance + * + * @template T = object + * + * @param {T} target The target class that concerns must be injected into + */ + public constructor(target: T) + { + this.#target = target; + } + + /** + * The target class + * + * @template T = object + * + * @returns {T} + */ + public get target(): T + { + return this.#target; + } + + /** + * Injects concern classes into the target class and return the modified target. + * + * **Note**: _Method performs injection in the following way:_ + * + * _**A**: Defines the concern classes in target class, via {@link defineConcerns}._ + * + * _**B**: Defines a concerns container in target class' prototype, via {@link defineContainer}._ + * + * _**C**: Defines "aliases" (proxy properties and methods) in target class' prototype, via {@link defineAliases}._ + * + * @template T = object The target class that concern classes must be injected into + * @template C = {@link Concern} + * + * @param {Constructor | Configuration} concerns List of concern classes / injection configurations + * + * @returns {MustUseConcerns} The modified target class + * + * @throws {InjectionException} + */ + public inject(...concerns: (Constructor|Configuration)[]): MustUseConcerns + { + // TODO: implement this method... + + return this.target as MustUseConcerns; + } + + /** + * Defines the concern classes that must be used by the target class. + * + * **Note**: _Method changes the target class, such that it implements and respects the + * {@link MustUseConcerns} interface. The original target class' constructor remains the untouched!_ + * + * @template T = object + * + * @param {T} target The target class that must define the concern classes to be used + * @param {Constructor[]} concerns List of concern classes + * + * @returns {MustUseConcerns} The modified target class + * + * @throws {InjectionException} If given concern classes conflict with target class' parent concern classes, + * e.g. in case of duplicates. Or, if unable to modify target class. + */ + public defineConcerns(target: T, concerns: Constructor[]): MustUseConcerns + { + // TODO: implement this method... + + return target as MustUseConcerns; + } + + /** + * Defines a concerns {@link Container} in target class' prototype. + * + * **Note**: _Method changes the target class, such that it implements and respects the + * [Owner]{@link import('@aedart/contracts/support/concerns').Owner} interface!_ + * + * @template T = object + * + * @param {MustUseConcerns} target The target in which a concerns container must be defined + * + * @returns {MustUseConcerns} The modified target class + * + * @throws {InjectionException} If unable to define concerns container in target class + */ + public defineContainer(target: MustUseConcerns): MustUseConcerns + { + // TODO: implement this method... + + return target; + } + + /** + * Defines "aliases" (proxy properties and methods) in target class' prototype, such that they + * point to the properties and methods available in the concern classes. + * + * **Note**: _Method defines each alias using the {@link defineAlias} method!_ + * + * @template T = object + * + * @param {MustUseConcerns} target The target in which "aliases" must be defined in + * @param {Configuration[]} configurations List of concern injection configurations + * + * @returns {MustUseConcerns} The modified target class + * + * @throws {InjectionException} If case of alias naming conflicts. Or, if unable to define aliases in target class. + */ + public defineAliases(target: MustUseConcerns, configurations: Configuration[]): MustUseConcerns + { + // TODO: implement this method... + + return target; + } + + /** + * Defines an "alias" (proxy property or method) in target class' prototype, to a property or method + * in given concern. + * + * **Note**: _Method will do nothing, if a property or method already exists in the target class' prototype + * chain, with the same name as given "alias"._ + * + * @template T = object + * + * @param {MustUseConcerns} target The target in which "alias" must be defined in + * @param {PropertyKey} alias Name of the "alias" in the target class (name of the proxy property or method) + * @param {PropertyKey} key Name of the property or method that the "alias" is for, in the concern class (`source`) + * @param {Constructor} source The concern class that holds the property or methods (`key`) + * + * @returns {boolean} `true` if "alias" was in target class. `false` if not, e.g. a property or method already + * exists in target class' prototype chain, with the same name as the alias. + * + * @throws {InjectionException} If unable to define "alias" in target class, e.g. due to failure when obtaining + * or defining [property descriptors]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#description}. + */ + public defineAlias( + target: MustUseConcerns, + alias: PropertyKey, + key: PropertyKey, + source: Constructor + ): boolean + { + // TODO: implement this method... + + return false; + } +} \ No newline at end of file diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index f2f7f1e3..1a600d5b 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -1,9 +1,11 @@ import AbstractConcern from "./AbstractConcern"; import ConcernsContainer from "./ConcernsContainer"; +import ConcernsInjector from "./ConcernsInjector"; export { AbstractConcern, - ConcernsContainer + ConcernsContainer, + ConcernsInjector }; export * from './exceptions'; \ No newline at end of file From 3e2ee0a4bc06b88af69aff8c1699e9527db87e94 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 15:30:06 +0100 Subject: [PATCH 124/424] Add use() class decorator --- packages/support/src/concerns/index.ts | 3 +- packages/support/src/concerns/use.ts | 38 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 packages/support/src/concerns/use.ts diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 1a600d5b..b650ecdb 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -8,4 +8,5 @@ export { ConcernsInjector }; -export * from './exceptions'; \ No newline at end of file +export * from './exceptions'; +export * from './use'; \ No newline at end of file diff --git a/packages/support/src/concerns/use.ts b/packages/support/src/concerns/use.ts new file mode 100644 index 00000000..98cb8866 --- /dev/null +++ b/packages/support/src/concerns/use.ts @@ -0,0 +1,38 @@ +import { + Concern, + Configuration +} from "@aedart/contracts/support/concerns"; +import type { Constructor } from "@aedart/contracts"; +import ConcernsInjector from "./ConcernsInjector"; + +/** + * Injects the given concern classes into target class + * + * **Note**: _Method is intended to be used as a decorator!_ + * + * **Example**: + * ``` + * @use( + * MyConcernA, + * MyConcernB, + * MyConcernC, + * ) + * class MyClass {} + * ``` + * + * @see Injector + * + * @template C = {@link Concern} + * + * @param {...Constructor | Configuration} concerns + * + * @returns {(target: object) => MustUseConcerns} + * + * @throws {InjectionException} + */ +export function use(...concerns: (Constructor|Configuration)[]) +{ + return (target: object) => { + return (new ConcernsInjector(target)).inject(...concerns); + } +} \ No newline at end of file From 44fdf89bac8df306c1355a6f5aac63c4d7329770 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 15:54:43 +0100 Subject: [PATCH 125/424] Add internal comments The prerequisite is that the concerns arguments are resolved to a list of "concern injection configuration" objects, which must be achieved first. --- packages/support/src/concerns/ConcernsInjector.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 2d050da1..d7cc234c 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -71,9 +71,17 @@ export default class ConcernsInjector implements Injector { // TODO: implement this method... + // Resolve arguments, such that they are of type "concern injection configuration". + + // A) Define the concern classes in target class + + // B) Define a concerns container in target class' prototype + + // C) Define "aliases" (proxy properties and methods) in target class' prototype + return this.target as MustUseConcerns; } - + /** * Defines the concern classes that must be used by the target class. * From f0f071e65bac9e03223ebea09532283dc7b2045c Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 16:34:31 +0100 Subject: [PATCH 126/424] Add isConcernClass util function --- packages/support/src/concerns/index.ts | 1 + .../support/src/concerns/isConcernClass.ts | 37 ++++++++++++++++ .../support/concerns/isConcernClass.test.js | 44 +++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 packages/support/src/concerns/isConcernClass.ts create mode 100644 tests/browser/packages/support/concerns/isConcernClass.test.js diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index b650ecdb..026d2421 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -9,4 +9,5 @@ export { }; export * from './exceptions'; +export * from './isConcernClass'; export * from './use'; \ No newline at end of file diff --git a/packages/support/src/concerns/isConcernClass.ts b/packages/support/src/concerns/isConcernClass.ts new file mode 100644 index 00000000..192602f4 --- /dev/null +++ b/packages/support/src/concerns/isConcernClass.ts @@ -0,0 +1,37 @@ +import { getClassPropertyDescriptors } from "@aedart/support/reflections"; +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; + +/** + * In-memory cache of classes that are determined to be of the type [Concern]{@link import('@aedart/contracts/support/concerns').Concern} + * + * @type {WeakSet} + */ +const concernClassCache: WeakSet = new WeakSet(); + +/** + * Determine if given target is a [Concern]{@link import('@aedart/contracts/support/concerns').Concern} class + * + * @param {object} target + * @param {boolean} [force=false] If `false` then cached result is returned + * + * @returns {boolean} + */ +export function isConcernClass(target: object, force: boolean = false): boolean +{ + if (!force && concernClassCache.has(target)) { + return true; + } + + try { + const descriptors = getClassPropertyDescriptors(target as ConstructorOrAbstractConstructor, true); + + if (Reflect.has(descriptors, 'concernOwner')) { + concernClassCache.add(parent); + return true; + } + + return false; + } catch (error) { + return false; + } +} \ No newline at end of file diff --git a/tests/browser/packages/support/concerns/isConcernClass.test.js b/tests/browser/packages/support/concerns/isConcernClass.test.js new file mode 100644 index 00000000..de005dfd --- /dev/null +++ b/tests/browser/packages/support/concerns/isConcernClass.test.js @@ -0,0 +1,44 @@ +import { isConcernClass, AbstractConcern } from "@aedart/support/concerns"; + +describe('@aedart/support/concerns', () => { + describe('isConcernClass()', () => { + + it('can determine if is a concern class', () => { + + const arr = []; + + class A {} + + class B { + get concernOwner() { + return false; + } + } + + class C extends AbstractConcern {} + + // ------------------------------------------------------------------------------------ // + + expect(isConcernClass(null)) + .withContext('Null is not a concern class') + .toBeFalse(); + + expect(isConcernClass(arr)) + .withContext('Array is not a concern class') + .toBeFalse(); + + expect(isConcernClass(A)) + .withContext('Class A is not a concern class') + .toBeFalse(); + + expect(isConcernClass(B)) + .withContext('Class B should be considered concern class') + .toBeTrue(); + + expect(isConcernClass(C)) + .withContext('Class C is concern class') + .toBeTrue(); + }); + + }); +}); \ No newline at end of file From 43e4498c527f44aed140fb9e27e41f5ebfa0f06c Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 18:32:08 +0100 Subject: [PATCH 127/424] Add additional case --- .../packages/support/concerns/isConcernClass.test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/browser/packages/support/concerns/isConcernClass.test.js b/tests/browser/packages/support/concerns/isConcernClass.test.js index de005dfd..83e7ad14 100644 --- a/tests/browser/packages/support/concerns/isConcernClass.test.js +++ b/tests/browser/packages/support/concerns/isConcernClass.test.js @@ -17,6 +17,8 @@ describe('@aedart/support/concerns', () => { class C extends AbstractConcern {} + class D extends C {} + // ------------------------------------------------------------------------------------ // expect(isConcernClass(null)) @@ -38,6 +40,10 @@ describe('@aedart/support/concerns', () => { expect(isConcernClass(C)) .withContext('Class C is concern class') .toBeTrue(); + + expect(isConcernClass(D)) + .withContext('Class D is concern class (inherits from Clas C)') + .toBeTrue(); }); }); From a8985c8a9a5454017a5b1ccb1c27b92b18253441 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 18:32:27 +0100 Subject: [PATCH 128/424] Add isSubclass() util function --- packages/support/src/reflections/index.ts | 3 +- .../support/src/reflections/isSubclass.ts | 19 +++++++++ .../support/reflections/isSubclass.test.js | 41 +++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 packages/support/src/reflections/isSubclass.ts create mode 100644 tests/browser/packages/support/reflections/isSubclass.test.js diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index 2c616ae9..d97a71b5 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -8,4 +8,5 @@ export * from './getParentOfClass'; export * from './hasPrototypeProperty'; export * from './isCallable'; export * from './isClassConstructor'; -export * from './isConstructor'; \ No newline at end of file +export * from './isConstructor'; +export * from './isSubclass'; \ No newline at end of file diff --git a/packages/support/src/reflections/isSubclass.ts b/packages/support/src/reflections/isSubclass.ts new file mode 100644 index 00000000..7575ae0f --- /dev/null +++ b/packages/support/src/reflections/isSubclass.ts @@ -0,0 +1,19 @@ +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { hasPrototypeProperty } from "./hasPrototypeProperty"; + +/** + * Determine if target class is a subclass (_child class_) of given superclass (_parent class_) + * + * @param {object} target + * @param {ConstructorOrAbstractConstructor} superclass + * + * @returns {boolean} `true` if target is a subclass of given superclass, `false` otherwise. + */ +export function isSubclass(target: object, superclass: ConstructorOrAbstractConstructor): boolean +{ + if (!hasPrototypeProperty(target) || !hasPrototypeProperty(superclass) || target === superclass) { + return false; + } + + return target.prototype instanceof superclass; +} \ No newline at end of file diff --git a/tests/browser/packages/support/reflections/isSubclass.test.js b/tests/browser/packages/support/reflections/isSubclass.test.js new file mode 100644 index 00000000..1ad66221 --- /dev/null +++ b/tests/browser/packages/support/reflections/isSubclass.test.js @@ -0,0 +1,41 @@ +import { isSubclass } from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('isSubclass()', () => { + + it('returns false if target has no prototype property', () => { + + const target = Object.create(null); + class A {} + + expect(isSubclass(target, A)) + .toBeFalse(); + }); + + it('returns false if superclass has no prototype property', () => { + + const target = Object.create(null); + class A {} + + expect(isSubclass(A, target)) + .toBeFalse(); + }); + + it('returns false when target and subclass params are the same', () => { + class A {} + + expect(isSubclass(A, A)) + .toBeFalse(); + }); + + it('can determine if target is a subclass', () => { + + class A {} + class B extends A {} + class C extends B {} + + expect(isSubclass(C, A)) + .toBeTrue(); + }); + }); +}); \ No newline at end of file From c2c1cdc53599075db7349f55f1a1a9e75dcfba6e Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 18:32:34 +0100 Subject: [PATCH 129/424] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef2ed5b3..29d5a18b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `getParentOfClass()` and `getAllParentsOfClass()` in `@aedart/support/reflections`. * `getClassPropertyDescriptor()` and `getClassPropertyDescriptors()` in `@aedart/support/reflections`. * `getConstructorName()` and `getNameOrDesc()` in `@aedart/support/reflections`. +* `isSubclass()` in `@aedart/support/reflections`. ## [0.8.0] - 2024-02-12 From e2c61bed875180a649feb18315e014dd7f0d7bfb Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 18:35:03 +0100 Subject: [PATCH 130/424] Simplify has prototype property check This appears to work as well, and a bit faster --- packages/support/src/reflections/hasPrototypeProperty.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/support/src/reflections/hasPrototypeProperty.ts b/packages/support/src/reflections/hasPrototypeProperty.ts index cb0957ce..3dbdac6f 100644 --- a/packages/support/src/reflections/hasPrototypeProperty.ts +++ b/packages/support/src/reflections/hasPrototypeProperty.ts @@ -11,7 +11,5 @@ */ export function hasPrototypeProperty(target: object): boolean { - return Reflect.has(target, 'prototype') - && target.prototype !== null - && typeof target.prototype == 'object'; + return typeof target.prototype == 'object' && target.prototype !== null; } \ No newline at end of file From 22411ae94a952ad18b5b8568a22adde08156423c Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 18:39:12 +0100 Subject: [PATCH 131/424] Fix cannot read property prototype on null --- packages/support/src/reflections/hasPrototypeProperty.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/reflections/hasPrototypeProperty.ts b/packages/support/src/reflections/hasPrototypeProperty.ts index 3dbdac6f..29ceddea 100644 --- a/packages/support/src/reflections/hasPrototypeProperty.ts +++ b/packages/support/src/reflections/hasPrototypeProperty.ts @@ -11,5 +11,5 @@ */ export function hasPrototypeProperty(target: object): boolean { - return typeof target.prototype == 'object' && target.prototype !== null; + return typeof target?.prototype == 'object' && target.prototype !== null; } \ No newline at end of file From eeb2c9dc3eb7ca484ac78296c6ddb4ffe55e2626 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 18:45:02 +0100 Subject: [PATCH 132/424] Add test of null as target --- .../support/reflections/hasPrototypeProperty.test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/browser/packages/support/reflections/hasPrototypeProperty.test.js b/tests/browser/packages/support/reflections/hasPrototypeProperty.test.js index e34156fd..a7d5a151 100644 --- a/tests/browser/packages/support/reflections/hasPrototypeProperty.test.js +++ b/tests/browser/packages/support/reflections/hasPrototypeProperty.test.js @@ -52,5 +52,12 @@ describe('@aedart/support/reflections', () => { .withContext('Class A should have a prototype') .toBeTrue(); }); + + it('returns false when null given', () => { + + expect(hasPrototypeProperty(null)) + .withContext('null does not have prototype') + .toBeFalse(); + }); }); }); \ No newline at end of file From a1b6abcc9974cc5f8a7159f09295e63d3aaf9cfc Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 18:51:18 +0100 Subject: [PATCH 133/424] Improve isConcernClass, check if target is subclass of Abstract Concern --- packages/support/src/concerns/isConcernClass.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/support/src/concerns/isConcernClass.ts b/packages/support/src/concerns/isConcernClass.ts index 192602f4..85882e9a 100644 --- a/packages/support/src/concerns/isConcernClass.ts +++ b/packages/support/src/concerns/isConcernClass.ts @@ -1,5 +1,6 @@ -import { getClassPropertyDescriptors } from "@aedart/support/reflections"; +import { isSubclass, getClassPropertyDescriptors } from "@aedart/support/reflections"; import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import AbstractConcern from "./AbstractConcern"; /** * In-memory cache of classes that are determined to be of the type [Concern]{@link import('@aedart/contracts/support/concerns').Concern} @@ -22,6 +23,11 @@ export function isConcernClass(target: object, force: boolean = false): boolean return true; } + if (isSubclass(target, AbstractConcern)) { + concernClassCache.add(parent); + return true; + } + try { const descriptors = getClassPropertyDescriptors(target as ConstructorOrAbstractConstructor, true); From 28d6ca12769a13964b9d0b7c7f68fe9f4262a6dd Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 18:52:06 +0100 Subject: [PATCH 134/424] Add test for case where target extends superclass that has concern owner --- .../packages/support/concerns/isConcernClass.test.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/browser/packages/support/concerns/isConcernClass.test.js b/tests/browser/packages/support/concerns/isConcernClass.test.js index 83e7ad14..089ee96c 100644 --- a/tests/browser/packages/support/concerns/isConcernClass.test.js +++ b/tests/browser/packages/support/concerns/isConcernClass.test.js @@ -18,6 +18,8 @@ describe('@aedart/support/concerns', () => { class C extends AbstractConcern {} class D extends C {} + + class E extends B {} // ------------------------------------------------------------------------------------ // @@ -42,7 +44,11 @@ describe('@aedart/support/concerns', () => { .toBeTrue(); expect(isConcernClass(D)) - .withContext('Class D is concern class (inherits from Clas C)') + .withContext('Class D is concern class (inherits from Class C)') + .toBeTrue(); + + expect(isConcernClass(E)) + .withContext('Class E should be considered a concern class (inherits from Class B)') .toBeTrue(); }); From c248e06972628b598a6054af4ff380ca323ace93 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 17 Feb 2024 19:06:25 +0100 Subject: [PATCH 135/424] Add another case --- .../browser/packages/support/reflections/isSubclass.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/browser/packages/support/reflections/isSubclass.test.js b/tests/browser/packages/support/reflections/isSubclass.test.js index 1ad66221..61b2f1d7 100644 --- a/tests/browser/packages/support/reflections/isSubclass.test.js +++ b/tests/browser/packages/support/reflections/isSubclass.test.js @@ -33,9 +33,14 @@ describe('@aedart/support/reflections', () => { class A {} class B extends A {} class C extends B {} + class D extends A {} expect(isSubclass(C, A)) .toBeTrue(); + + expect(isSubclass(D, B)) + .withContext('D should not be a subclass of B') + .toBeFalse(); }); }); }); \ No newline at end of file From 36b86ea34e241732950a1412d66eb884302a2d9b Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 18 Feb 2024 12:18:55 +0100 Subject: [PATCH 136/424] Improve method description --- packages/support/src/reflections/isSubclass.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/support/src/reflections/isSubclass.ts b/packages/support/src/reflections/isSubclass.ts index 7575ae0f..d1199a41 100644 --- a/packages/support/src/reflections/isSubclass.ts +++ b/packages/support/src/reflections/isSubclass.ts @@ -4,6 +4,10 @@ import { hasPrototypeProperty } from "./hasPrototypeProperty"; /** * Determine if target class is a subclass (_child class_) of given superclass (_parent class_) * + * **Note**: _Method determines if target is a child of given superclass, by checking if the `target.prototype` + * is an instance of given superclass (`target.prototype instanceof superclass`) + * However, if given target or superclass does not have a prototype property, then `false` is returned._ + * * @param {object} target * @param {ConstructorOrAbstractConstructor} superclass * From bb892ed3441a07f3aab688a8b44c36ef351c6a0c Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 18 Feb 2024 15:27:34 +0100 Subject: [PATCH 137/424] Init contracts support objects submodule --- aliases.js | 1 + packages/contracts/package.json | 5 +++++ packages/contracts/src/support/objects/index.ts | 8 ++++++++ packages/contracts/src/support/objects/types.ts | 2 ++ packages/support/rollup.config.mjs | 1 + 5 files changed, 17 insertions(+) create mode 100644 packages/contracts/src/support/objects/index.ts create mode 100644 packages/contracts/src/support/objects/types.ts diff --git a/aliases.js b/aliases.js index b5856900..b5198548 100644 --- a/aliases.js +++ b/aliases.js @@ -23,6 +23,7 @@ module.exports = { '@aedart/contracts/support/exceptions': path.resolve(__dirname, './packages/contracts/support/exceptions'), '@aedart/contracts/support/meta': path.resolve(__dirname, './packages/contracts/support/meta'), '@aedart/contracts/support/mixins': path.resolve(__dirname, './packages/contracts/support/mixins'), + '@aedart/contracts/support/objects': path.resolve(__dirname, './packages/contracts/support/objects'), '@aedart/contracts/support/reflections': path.resolve(__dirname, './packages/contracts/support/reflections'), '@aedart/contracts/support': path.resolve(__dirname, './packages/contracts/support'), '@aedart/contracts': path.resolve(__dirname, './packages/contracts/src'), diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 60c2e5ff..1ecc1d67 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -56,6 +56,11 @@ "import": "./dist/esm/support/mixins.js", "require": "./dist/cjs/support/mixins.cjs" }, + "./support/objects": { + "types": "./dist/types/support/objects.d.ts", + "import": "./dist/esm/support/objects.js", + "require": "./dist/cjs/support/objects.cjs" + }, "./support/reflections": { "types": "./dist/types/support/reflections.d.ts", "import": "./dist/esm/support/reflections.js", diff --git a/packages/contracts/src/support/objects/index.ts b/packages/contracts/src/support/objects/index.ts new file mode 100644 index 00000000..7a145322 --- /dev/null +++ b/packages/contracts/src/support/objects/index.ts @@ -0,0 +1,8 @@ +/** + * Support Objects identifier + * + * @type {Symbol} + */ +export const SUPPORT_OBJECTS: unique symbol = Symbol('@aedart/contracts/support/objects'); + +export * from './types'; \ No newline at end of file diff --git a/packages/contracts/src/support/objects/types.ts b/packages/contracts/src/support/objects/types.ts new file mode 100644 index 00000000..89f8f836 --- /dev/null +++ b/packages/contracts/src/support/objects/types.ts @@ -0,0 +1,2 @@ +// TODO: Replace this... +export type TMP = string; \ No newline at end of file diff --git a/packages/support/rollup.config.mjs b/packages/support/rollup.config.mjs index a0d97e13..cc3b6175 100644 --- a/packages/support/rollup.config.mjs +++ b/packages/support/rollup.config.mjs @@ -9,6 +9,7 @@ export default createConfig({ '@aedart/contracts/support/exceptions', '@aedart/contracts/support/meta', '@aedart/contracts/support/mixins', + '@aedart/contracts/support/objects', '@aedart/contracts/support/reflections', ] }); From f5767cc0b5ddd80085eb6bc5be5dd3a7cb9dc265 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 18 Feb 2024 15:27:44 +0100 Subject: [PATCH 138/424] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29d5a18b..5d691d52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * `@aedart/contracts/support/exceptions` and `@aedart/support/exceptions` submodules. +* `@aedart/contracts/support/objects` submodule. * `Throwable` (_extends TypeScript's `Error` interface_) interface in `@aedart/contracts/support/exceptions`. * `LogicalError` and `AbstractClassError` exceptions in `@aedart/support/exceptions`. * `FUNCTION_PROTOTYPE` const in `@aedart/contracts/support/reflections`. From 7b6f2bf8453e1cde166839fd189e6b211f2b9e7b Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 18 Feb 2024 18:56:17 +0100 Subject: [PATCH 139/424] Add merge() util method --- .../src/support/objects/MergeOptions.ts | 51 +++ .../contracts/src/support/objects/index.ts | 12 + .../contracts/src/support/objects/types.ts | 15 +- .../src/objects/exceptions/MergeError.ts | 28 ++ .../support/src/objects/exceptions/index.ts | 4 + packages/support/src/objects/index.ts | 5 +- packages/support/src/objects/merge.ts | 194 +++++++++++ .../packages/support/objects/merge.test.js | 312 ++++++++++++++++++ 8 files changed, 618 insertions(+), 3 deletions(-) create mode 100644 packages/contracts/src/support/objects/MergeOptions.ts create mode 100644 packages/support/src/objects/exceptions/MergeError.ts create mode 100644 packages/support/src/objects/exceptions/index.ts create mode 100644 packages/support/src/objects/merge.ts create mode 100644 tests/browser/packages/support/objects/merge.test.js diff --git a/packages/contracts/src/support/objects/MergeOptions.ts b/packages/contracts/src/support/objects/MergeOptions.ts new file mode 100644 index 00000000..bc8195ef --- /dev/null +++ b/packages/contracts/src/support/objects/MergeOptions.ts @@ -0,0 +1,51 @@ +import type { MergeCallback } from "./types"; + +/** + * Merge Options + */ +export default interface MergeOptions +{ + /** + * Property Keys to be skipped + * + * **Note**: _Defaults to [DEFAULT_MERGE_SKIP_KEYS]{@link import('@aedart/contracts/support/objects').DEFAULT_MERGE_SKIP_KEYS} + * when not specified._ + * + * @type {PropertyKey[]} + */ + skip?: PropertyKey[]; + + /** + * Flag, overwrite property values with `undefined`. + * + * **When `true` (_default behaviour_)**: _If an existing property value is not `undefined`, it will be overwritten + * with new value, even if the new value is `undefined`._ + * + * **When `false`**: _If an existing property value is not `undefined`, it will NOT be overwritten + * with new value, if the new value is `undefined`._ + * + * @type {boolean} + */ + overwriteWithUndefined?: boolean; + + /** + * Flag, merge array properties + * + * **When `true`**: _existing array property value is attempted merged with new array value._ + * + * **When `false` (_default behaviour_)**: _existing array property value is overwritten with new array value_ + * + * @type {boolean} + */ + mergeArrays?: boolean, + + /** + * The merge callback that must be applied + * + * **Note**: _When no callback is provided, then a default is + * set by the merge function_ + * + * @type {MergeCallback} + */ + callback?: MergeCallback; +} \ No newline at end of file diff --git a/packages/contracts/src/support/objects/index.ts b/packages/contracts/src/support/objects/index.ts index 7a145322..e7650987 100644 --- a/packages/contracts/src/support/objects/index.ts +++ b/packages/contracts/src/support/objects/index.ts @@ -5,4 +5,16 @@ */ export const SUPPORT_OBJECTS: unique symbol = Symbol('@aedart/contracts/support/objects'); +/** + * Default property keys to be skipped when merging objects + * + * @type {PropertyKey[]} + */ +export const DEFAULT_MERGE_SKIP_KEYS: PropertyKey[] = [ 'prototype', '__proto__' ]; + +import MergeOptions from "./MergeOptions"; +export { + type MergeOptions +} + export * from './types'; \ No newline at end of file diff --git a/packages/contracts/src/support/objects/types.ts b/packages/contracts/src/support/objects/types.ts index 89f8f836..f172327b 100644 --- a/packages/contracts/src/support/objects/types.ts +++ b/packages/contracts/src/support/objects/types.ts @@ -1,2 +1,13 @@ -// TODO: Replace this... -export type TMP = string; \ No newline at end of file +import MergeOptions from "./MergeOptions"; + +/** + * Merge callback function + */ +export type MergeCallback = ( + result: object, + key: PropertyKey, + value: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + source: object, + sourceIndex: number, + options: MergeOptions +) => any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ \ No newline at end of file diff --git a/packages/support/src/objects/exceptions/MergeError.ts b/packages/support/src/objects/exceptions/MergeError.ts new file mode 100644 index 00000000..84d7b44b --- /dev/null +++ b/packages/support/src/objects/exceptions/MergeError.ts @@ -0,0 +1,28 @@ +import type { Throwable } from "@aedart/contracts/support/exceptions"; + +/** + * Merge Error + * + * To be thrown when two or more objects are unable to be merged. + */ +export default class MergeError extends Error implements Throwable +{ + /** + * Create a new Merge Error instance + * + * @param {string} [message] + * @param {ErrorOptions} [options] + */ + constructor(message?: string, options?: ErrorOptions) + { + super(message, options); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, MergeError); + } else { + this.stack = (new Error()).stack; + } + + this.name = "MergeError"; + } +} \ No newline at end of file diff --git a/packages/support/src/objects/exceptions/index.ts b/packages/support/src/objects/exceptions/index.ts new file mode 100644 index 00000000..4d7f6520 --- /dev/null +++ b/packages/support/src/objects/exceptions/index.ts @@ -0,0 +1,4 @@ +import MergeError from "./MergeError"; +export { + MergeError +} \ No newline at end of file diff --git a/packages/support/src/objects/index.ts b/packages/support/src/objects/index.ts index 305dd627..a810f42f 100644 --- a/packages/support/src/objects/index.ts +++ b/packages/support/src/objects/index.ts @@ -6,10 +6,13 @@ export * from './hasAll'; export * from './hasAny'; export * from './hasUniqueId'; export * from './isset'; +export * from './merge'; export * from './set'; export * from './uniqueId'; import ObjectId from "./ObjectId"; export { ObjectId -} \ No newline at end of file +} + +export * from './exceptions'; \ No newline at end of file diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts new file mode 100644 index 00000000..15819135 --- /dev/null +++ b/packages/support/src/objects/merge.ts @@ -0,0 +1,194 @@ +import { + MergeOptions, + MergeCallback, DEFAULT_MERGE_SKIP_KEYS +} from "@aedart/contracts/support/objects"; +import { isPrimitive, descTag } from "@aedart/support/misc"; +import MergeError from "./exceptions/MergeError"; + + +/** + * Merge two or more objects + * + * @param {object[]} sources + * @param {MergeOptions} [options] The `callback` setting defaults to {@link defaultMergeCallback} when none given + * + * @returns {object} + * + * @throws {MergeError} If unable to merge objects + */ +export function merge( + sources: object[], + options?: MergeOptions +): object +{ + // Resolve defaults + const defaults: MergeOptions = { + skip: DEFAULT_MERGE_SKIP_KEYS, + overwriteWithUndefined: true, + mergeArrays: false, + callback: defaultMergeCallback + }; + const resolvedOptions: MergeOptions = { ...defaults, ...options }; + + // Perform actual merge... + try { + return performMerge(sources, resolvedOptions); + } catch (error) { + if (error instanceof MergeError) { + throw error; + } + + throw new MergeError('Unable to merge objects', { + cause: { + error: error, + sources: sources, + options: options + } + }); + } +} + +/** + * Performs merge of given objects + * + * @internal + * + * @param {object[]} sources + * @param {MergeOptions} options + * + * @returns {object} + * + * @throws {MergeError} If unable to merge objects + */ +function performMerge(sources: object[], options: MergeOptions): object +{ + return sources.reduce((result: object, current: object, index: number) => { + if (Array.isArray(current)) { + throw new MergeError(`Unable to merge object with an array source, (source index: ${index})`, { + cause: { + current: current, + index: index + } + }); + } + + const keys: PropertyKey[] = Reflect.ownKeys(current); + for (const key of keys){ + // Skip key if needed ... + if (options.skip?.includes(key) === true) { + continue; + } + + // Resolve the value via callback and set it in resulting object. + result[key] = options.callback( + result, + key, + current[key], + current, + index, + options + ); + } + + return result; + }, Object.create(null)); +} + +/** + * Default merge callback + * + * @param {object} result The final resulting object + * @param {PropertyKey} key + * @param {any} value + * @param {object} source + * @param {number} sourceIndex + * @param {MergeOptions} options + * + * @returns {any} The value to be merged into the resulting object + * + * @throws {MergeError} If unable to resolve value + */ +export const defaultMergeCallback: MergeCallback = function( + result: object, + key: PropertyKey, + value: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + source: object, + sourceIndex: number, + options: MergeOptions +): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +{ + // Determine if an existing property exists, and its type... + const exists: boolean = Reflect.has(result, key); + const type: string = typeof value; + + // Primitives + // @see https://developer.mozilla.org/en-US/docs/Glossary/Primitive + if (isPrimitive(value)) { + + // Do not overwrite existing value with `undefined`, if options do not allow it... + if (value === undefined && options.overwriteWithUndefined === false && exists && result[key] !== undefined) { + return result[key]; + } + + // Otherwise, just return the value for primitive value. + return value; + } + + // Arrays + if (Array.isArray(value)) { + + // Create a structured clone of array value. However, this can fail if array contains + // complex values, e.g. functions, objects, ...etc. + try { + const clonedArray = structuredClone(value); + + // Merge array values if required. + if (options.mergeArrays === true && exists && Array.isArray(result[key])) { + return [ ...result[key], ...clonedArray ]; + } + + // Returns the cloned array, to avoid unintended manipulation... + return clonedArray; + } catch (error) { + throw new MergeError(`Unable to merge array value at source index ${sourceIndex}`, { + cause: { + error: error, + key: key, + value: value, + source: source, + sourceIndex: sourceIndex, + options: options + } + }); + } + } + // TODO: Array-Like objects ??? + + // Functions are an exception to the copy rule. They must remain untouched... + if (type == 'function') { + return value; + } + + // Objects + if (type == 'object') { + // Merge with existing, if not null... + if (exists && typeof result[key] == 'object' && result[key] !== null) { + return performMerge([ result[key], value ], options); + } + + // Otherwise, create a new object and merge it. + return performMerge([ {}, value ], options); + } + + // If for some reason this point is reached, it means that we are unable to merge "something". + throw new MergeError(`Unable to merge value of type ${type} (${descTag(value)}) at source index ${sourceIndex}`, { + cause: { + key: key, + value: value, + source: source, + sourceIndex: sourceIndex, + options: options + } + }); +} + diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js new file mode 100644 index 00000000..c37cee61 --- /dev/null +++ b/tests/browser/packages/support/objects/merge.test.js @@ -0,0 +1,312 @@ +import { DEFAULT_MERGE_SKIP_KEYS } from "@aedart/contracts/support/objects"; +import { + merge, + MergeError +} from "@aedart/support/objects"; + +describe('@aedart/support/objects', () => { + describe('merge', () => { + + it('can merge primitive values', () => { + + const MY_SYMBOL_A = Symbol('a'); + const MY_SYMBOL_B = Symbol('b'); + + const a = { + 'string': 'hi', + 'int': 1, + 'float': 1.5, + 'bigint': BigInt(9000000000000000), + 'boolean': true, + 'undefined': undefined, // Redundant... + 'symbol': MY_SYMBOL_A, + 'null': null, // Redundant... + }; + const b = { + 'string': 'there', + 'int': 2, + 'float': 2.3, + 'bigint': BigInt(9000000000000001), + 'boolean': false, + 'undefined': undefined, // Redundant... + 'symbol': MY_SYMBOL_B, + 'null': null, // Redundant... + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ]); + + // Debug + //console.log('result', result); + + const keys = Reflect.ownKeys(result); + expect(keys.length) + .withContext('Incorrect amount of keys merged') + .toBe(Reflect.ownKeys(b).length); + + for (const key of keys) { + const expected = b[key]; + + expect(result[key]) + .withContext(`Incorrect value for key ${key}`) + .toBe(expected); + } + }); + + it('overwrites existing values with undefined be default', () => { + const a = { + 'foo': 'bar' + }; + const b = { + 'foo': undefined + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ]); + + // Debug + //console.log('result', result); + + expect(result['foo']) + .withContext('Value should be undefined') + .toBeUndefined(); + }); + + it('can avoid overwriting existing values with undefined', () => { + const a = { + 'foo': 'bar' + }; + const b = { + 'foo': undefined + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ], { overwriteWithUndefined: false }); + + // Debug + // console.log('result', result); + + expect(result['foo']) + .withContext('Value should NOT be undefined') + .toBe(a['foo']) + }); + + it('skips default keys', () => { + const a = { + 'foo': 'bar' + }; + const b = { + prototype: { 'bar': true } + }; + const c = { + __proto__: { 'zar': false } + } + + // --------------------------------------------------------------------- // + + const result = merge([ a, b, c ]); + + // Debug + // console.log('result', result); + + for (const key of DEFAULT_MERGE_SKIP_KEYS) { + expect(Reflect.has(result, key)) + .withContext(`Default skip key (${key}) is not skipped`) + .toBeFalse(); + } + }); + + it('can skip custom provided keys', () => { + const a = { + 'foo': 'bar' + }; + const b = { + 'bar': 'foo' + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ], { skip: [ 'foo' ] }); + + // Debug + // console.log('result', result); + + expect(Reflect.has(result, 'bar')) + .withContext('None skipped key missing') + .toBeTrue(); + + expect(Reflect.has(result, 'foo')) + .withContext('Skipped key is in output') + .toBeFalse(); + }); + + it('can merge values of symbol keys', () => { + + const MY_SYMBOL_KEY = Symbol('a'); + + const a = { + [MY_SYMBOL_KEY]: true + }; + const b = { + [MY_SYMBOL_KEY]: 'Wee' + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ]); + + // Debug + //console.log('result', result); + + const keys = Reflect.ownKeys(result); + expect(keys.length) + .withContext('Incorrect amount of keys merged') + .toBe(Reflect.ownKeys(b).length); + + for (const key of keys) { + const expected = b[key]; + + expect(result[key]) + .withContext(`Incorrect value for symbol key`) + .toBe(expected); + } + }); + + it('overwrites array properties by default', () => { + + const a = { + 'arr': [ 1, 2, 3 ] + }; + const b = { + 'arr': [ 4, 5, 6 ] + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ]); + + // Debug + // console.log('result', result); + + const expected = JSON.stringify(b); + expect(JSON.stringify(result)) + .withContext('Array value not overwritten by default') + .toBe(expected); + + expect(result['arr'] === b['arr']) + .withContext('Array property is not copied (structured copy was expected)') + .toBeFalse(); + }); + + it('can merge array values', () => { + + const a = { + 'arr': [ 1, 2, 3 ] + }; + const b = { + 'arr': [ 4, 5, 6 ] + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ], { mergeArrays: true }); + + // Debug + // console.log('result', result); + + const expected = JSON.stringify({ + 'arr': [ ...a['arr'], ...b['arr'] ] + }); + expect(JSON.stringify(result)) + .withContext('Array value not merged') + .toBe(expected); + }); + + it('fails when array values contain none-cloneable values', () => { + + const callback = () => { + const a = { + 'arr': [ 1, 2, 3 ] + }; + const b = { + 'arr': [ function() {} ] + }; + + return merge([ a, b ] ); + } + + // --------------------------------------------------------------------- // + + expect(callback) + .toThrowError(MergeError); + }); + + it('creates shallow copy of functions', () => { + const a = { + 'foo': null, + }; + const b = { + 'foo': function() {} + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ], { mergeArrays: true }); + + expect(Reflect.has(result, 'foo')) + .withContext('Key with function value not merged') + .toBeTrue(); + + expect(result['foo']) + .withContext('Function not referenced in result') + .toBe(b['foo']) + }); + + it('can merge nested objects', () => { + const a = { + 'foo': null, + 'bar': { + 'name': 'Risk' + } + }; + const b = { + 'foo': { + 'name': 'John' + }, + 'bar': { + 'age': 31, + 'address': { + 'street': 'Somewhere Str. 654' + } + } + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ], { mergeArrays: true }); + + // Debug + console.log('result', result) + + const expected = JSON.stringify({ + 'foo': { + 'name': 'John' + }, + 'bar': { + 'name': 'Risk', + 'age': 31, + 'address': { + 'street': 'Somewhere Str. 654' + } + } + }); + + expect(JSON.stringify(result)) + .withContext('Incorrect merge of nested objects') + .toBe(expected) + }); + }); +}); \ No newline at end of file From c5d9d8c1d2682182c372b719e2a353fcc96ed00c Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 18 Feb 2024 18:56:27 +0100 Subject: [PATCH 140/424] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d691d52..4b87a14b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `getClassPropertyDescriptor()` and `getClassPropertyDescriptors()` in `@aedart/support/reflections`. * `getConstructorName()` and `getNameOrDesc()` in `@aedart/support/reflections`. * `isSubclass()` in `@aedart/support/reflections`. +* `merge()` in `@aedart/support/objects`. ## [0.8.0] - 2024-02-12 From 48267b30750521399459f45201e9f43495b61e03 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 18 Feb 2024 19:01:14 +0100 Subject: [PATCH 141/424] Fix property descriptors not deep merged As an edge case, if a superclass defines a getter and the subclass defines a setter for a property, then the property descriptor of the top-most class will prevail, which is not desired. Therefore, a descriptor object must always be deep merged. --- .../getClassPropertyDescriptors.ts | 3 +- .../getClassPropertyDescriptors.test.js | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/support/src/reflections/getClassPropertyDescriptors.ts b/packages/support/src/reflections/getClassPropertyDescriptors.ts index fe7cb062..a0679bdb 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptors.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptors.ts @@ -2,6 +2,7 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import { getClassPropertyDescriptor } from "./getClassPropertyDescriptor"; import { assertHasPrototypeProperty } from "./assertHasPrototypeProperty"; import { getAllParentsOfClass } from "./getAllParentsOfClass"; +import { merge } from "@aedart/support/objects"; /** * Returns all property descriptors that are defined target's prototype @@ -45,7 +46,7 @@ export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstru // Merge evt. existing descriptor object with the one obtained from target. if (Reflect.has(output, key)) { - output[key] = Object.assign(output[key], descriptor); + output[key] = merge([ output[key], descriptor ], { overwriteWithUndefined: false }); continue; } diff --git a/tests/browser/packages/support/reflections/getClassPropertyDescriptors.test.js b/tests/browser/packages/support/reflections/getClassPropertyDescriptors.test.js index 1e2f63ed..05e77b4c 100644 --- a/tests/browser/packages/support/reflections/getClassPropertyDescriptors.test.js +++ b/tests/browser/packages/support/reflections/getClassPropertyDescriptors.test.js @@ -177,6 +177,38 @@ describe('@aedart/support/reflections', () => { // console.log('- - - '.repeat(15)); } }); + + it('merges property descriptors', () => { + + class A { + get age() {} + } + + class B extends A { + set age(value) {} + } + + // -------------------------------------------------------------------------------- // + + const descriptors = getClassPropertyDescriptors(B, true); + + expect(Reflect.has(descriptors, 'age')) + .withContext('age property descriptor not in output') + .toBeTrue(); + + const ageDesc = descriptors['age']; + + // Debug + // console.log('Age property descriptor', ageDesc); + + expect(typeof ageDesc.set !== 'undefined') + .withContext('set() function not specified in descriptor') + .toBeTrue(); + + expect(typeof ageDesc.get !== 'undefined') + .withContext('get() function not specified in descriptor') + .toBeTrue(); + }); }); }); \ No newline at end of file From 00dcc938012843107b926c5d413ae925e512d970 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 18 Feb 2024 19:12:09 +0100 Subject: [PATCH 142/424] Add classOwnKeys() util function --- .../support/src/reflections/classOwnKeys.ts | 36 +++++++++++++++++++ packages/support/src/reflections/index.ts | 1 + 2 files changed, 37 insertions(+) create mode 100644 packages/support/src/reflections/classOwnKeys.ts diff --git a/packages/support/src/reflections/classOwnKeys.ts b/packages/support/src/reflections/classOwnKeys.ts new file mode 100644 index 00000000..4c0333e4 --- /dev/null +++ b/packages/support/src/reflections/classOwnKeys.ts @@ -0,0 +1,36 @@ +import type {ConstructorOrAbstractConstructor} from "@aedart/contracts"; +import { assertHasPrototypeProperty } from "@aedart/support/reflections/assertHasPrototypeProperty"; +import { getAllParentsOfClass } from "@aedart/support/reflections/getAllParentsOfClass"; + +/** + * Returns property keys that are defined target's prototype + * + * @param {ConstructorOrAbstractConstructor} target + * @param {boolean} [recursive=false] If `true`, then target's parent prototypes are traversed and all + * property keys are returned. + * + * @returns {PropertyKey[]} + * + * @throws {TypeError} If target object does not have "prototype" property + */ +export function classOwnKeys(target: ConstructorOrAbstractConstructor, recursive: boolean = false): PropertyKey[] +{ + assertHasPrototypeProperty(target); + + if (!recursive) { + return Reflect.ownKeys(target.prototype); + } + + // Obtain target's parent classes... + const parents = getAllParentsOfClass(target.prototype, true); + + const ownKeys: Set = new Set(); + for (const parent of parents) { + const keys: PropertyKey[] = Reflect.ownKeys(parent.prototype); + for (const key of keys) { + ownKeys.add(key); + } + } + + return Array.from(ownKeys); +} \ No newline at end of file diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index d97a71b5..77ff7e62 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -1,4 +1,5 @@ export * from './assertHasPrototypeProperty'; +export * from './classOwnKeys'; export * from './getAllParentsOfClass'; export * from './getClassPropertyDescriptor'; export * from './getClassPropertyDescriptors'; From 6525f0d1d876b2306bba8b5dcd2d4a77ce3f208c Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 18 Feb 2024 19:12:15 +0100 Subject: [PATCH 143/424] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b87a14b..0efca248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `getClassPropertyDescriptor()` and `getClassPropertyDescriptors()` in `@aedart/support/reflections`. * `getConstructorName()` and `getNameOrDesc()` in `@aedart/support/reflections`. * `isSubclass()` in `@aedart/support/reflections`. +* `classOwnKeys()` in `@aedart/support/reflections`. * `merge()` in `@aedart/support/objects`. ## [0.8.0] - 2024-02-12 From c8c292273547c0e36cc14a9093c1d2b4d432cf6b Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 18 Feb 2024 19:15:57 +0100 Subject: [PATCH 144/424] Cleanup --- tests/browser/packages/support/objects/merge.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js index c37cee61..cb550f84 100644 --- a/tests/browser/packages/support/objects/merge.test.js +++ b/tests/browser/packages/support/objects/merge.test.js @@ -289,7 +289,7 @@ describe('@aedart/support/objects', () => { const result = merge([ a, b ], { mergeArrays: true }); // Debug - console.log('result', result) + // console.log('result', result) const expected = JSON.stringify({ 'foo': { From 188163d4a68a295ed8cd86160fce95b803153d06 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 07:09:24 +0100 Subject: [PATCH 145/424] Extract default merge options into own const Also, now allowing to specify merge callback directly as second argument. --- packages/support/src/objects/merge.ts | 50 ++++++++++++++++++++------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index 15819135..dbb58b0b 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -5,12 +5,25 @@ import { import { isPrimitive, descTag } from "@aedart/support/misc"; import MergeError from "./exceptions/MergeError"; +/** + * Default merge options to be applied, when none are provided to {@link merge} + * + * @type {MergeOptions} + */ +export const DEFAULT_MERGE_OPTIONS: MergeOptions = { + skip: DEFAULT_MERGE_SKIP_KEYS, + overwriteWithUndefined: true, + mergeArrays: false, +}; +Object.freeze(DEFAULT_MERGE_OPTIONS); /** * Merge two or more objects * * @param {object[]} sources - * @param {MergeOptions} [options] The `callback` setting defaults to {@link defaultMergeCallback} when none given + * @param {MergeCallback|MergeOptions} [options] Merge callback or merge options. If merge options are given, + * then the `callback` setting is automatically set to {@link defaultMergeCallback} + * if not otherwise specified. * * @returns {object} * @@ -18,23 +31,36 @@ import MergeError from "./exceptions/MergeError"; */ export function merge( sources: object[], - options?: MergeOptions + options?: MergeCallback | MergeOptions ): object { - // Resolve defaults - const defaults: MergeOptions = { - skip: DEFAULT_MERGE_SKIP_KEYS, - overwriteWithUndefined: true, - mergeArrays: false, - callback: defaultMergeCallback - }; - const resolvedOptions: MergeOptions = { ...defaults, ...options }; + // Resolve merge callback + const callback: MergeCallback = (typeof options == 'function') + ? options + : defaultMergeCallback; + // Resolve user provided merge options + const userOptions: MergeOptions = (typeof options == 'object' && options !== null) + ? options + : Object.create(null) + + // Resolve the final options to use + const resolved: MergeOptions = { + ...DEFAULT_MERGE_OPTIONS, + ...{ + callback: callback + }, + ...userOptions + }; + // Perform actual merge... try { - return performMerge(sources, resolvedOptions); + return performMerge(sources, resolved); } catch (error) { if (error instanceof MergeError) { + error.cause.sources = sources; + error.cause.options = resolved; + throw error; } @@ -42,7 +68,7 @@ export function merge( cause: { error: error, sources: sources, - options: options + options: resolved } }); } From 17e8b02cb36f9da78595f353b4b00f6066c3445e Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 08:31:59 +0100 Subject: [PATCH 146/424] Refactor skip logic, allow for custom callback --- .../src/support/objects/MergeOptions.ts | 11 +- .../contracts/src/support/objects/types.ts | 19 ++- packages/support/src/objects/merge.ts | 136 +++++++++++------- 3 files changed, 106 insertions(+), 60 deletions(-) diff --git a/packages/contracts/src/support/objects/MergeOptions.ts b/packages/contracts/src/support/objects/MergeOptions.ts index bc8195ef..d49800fd 100644 --- a/packages/contracts/src/support/objects/MergeOptions.ts +++ b/packages/contracts/src/support/objects/MergeOptions.ts @@ -1,4 +1,4 @@ -import type { MergeCallback } from "./types"; +import type { MergeCallback, SkipKeyCallback } from "./types"; /** * Merge Options @@ -6,14 +6,17 @@ import type { MergeCallback } from "./types"; export default interface MergeOptions { /** - * Property Keys to be skipped + * Property Keys that must not be merged. * * **Note**: _Defaults to [DEFAULT_MERGE_SKIP_KEYS]{@link import('@aedart/contracts/support/objects').DEFAULT_MERGE_SKIP_KEYS} * when not specified._ * - * @type {PropertyKey[]} + * **Callback**: _A callback can be specified to determine if a given key, + * in a source object should be skipped._ + * + * @type {PropertyKey[] | SkipKeyCallback} */ - skip?: PropertyKey[]; + skip?: PropertyKey[] | SkipKeyCallback; /** * Flag, overwrite property values with `undefined`. diff --git a/packages/contracts/src/support/objects/types.ts b/packages/contracts/src/support/objects/types.ts index f172327b..f45de7b4 100644 --- a/packages/contracts/src/support/objects/types.ts +++ b/packages/contracts/src/support/objects/types.ts @@ -2,6 +2,13 @@ import MergeOptions from "./MergeOptions"; /** * Merge callback function + * + * The callback is responsible for resolving the value to be merged into the resulting object. + * + * **Note**: _[Skipped keys]{@link MergeOptions.skip} are NOT provided to the callback._ + * + * **Note**: _The callback is responsible for respecting the given [options]{@link MergeOptions}, + * ([keys to be skipped]{@link MergeOptions.skip} and max depth reach excluded)_ */ export type MergeCallback = ( result: object, @@ -10,4 +17,14 @@ export type MergeCallback = ( source: object, sourceIndex: number, options: MergeOptions -) => any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ \ No newline at end of file +) => any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + +/** + * A callback that determines if property key should be skipped + * + * If the callback returns `true`, then the given key will NOT be merged + * from the given source object. + * + * @see {MergeOptions} + */ +export type SkipKeyCallback = (key: PropertyKey, source: object, result: object) => boolean; \ No newline at end of file diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index dbb58b0b..9716981d 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -1,7 +1,9 @@ -import { +import type { MergeOptions, - MergeCallback, DEFAULT_MERGE_SKIP_KEYS + MergeCallback, + SkipKeyCallback } from "@aedart/contracts/support/objects"; +import { DEFAULT_MERGE_SKIP_KEYS } from "@aedart/contracts/support/objects"; import { isPrimitive, descTag } from "@aedart/support/misc"; import MergeError from "./exceptions/MergeError"; @@ -42,9 +44,9 @@ export function merge( // Resolve user provided merge options const userOptions: MergeOptions = (typeof options == 'object' && options !== null) ? options - : Object.create(null) + : Object.create(null); - // Resolve the final options to use + // Merge the default and user provided options... const resolved: MergeOptions = { ...DEFAULT_MERGE_OPTIONS, ...{ @@ -53,6 +55,11 @@ export function merge( ...userOptions }; + // Resolve the skip callback. + if (Array.isArray(resolved.skip)) { + resolved.skip = makeDefaultSkipCallback(resolved.skip); + } + // Perform actual merge... try { return performMerge(sources, resolved); @@ -74,60 +81,14 @@ export function merge( } } -/** - * Performs merge of given objects - * - * @internal - * - * @param {object[]} sources - * @param {MergeOptions} options - * - * @returns {object} - * - * @throws {MergeError} If unable to merge objects - */ -function performMerge(sources: object[], options: MergeOptions): object -{ - return sources.reduce((result: object, current: object, index: number) => { - if (Array.isArray(current)) { - throw new MergeError(`Unable to merge object with an array source, (source index: ${index})`, { - cause: { - current: current, - index: index - } - }); - } - - const keys: PropertyKey[] = Reflect.ownKeys(current); - for (const key of keys){ - // Skip key if needed ... - if (options.skip?.includes(key) === true) { - continue; - } - - // Resolve the value via callback and set it in resulting object. - result[key] = options.callback( - result, - key, - current[key], - current, - index, - options - ); - } - - return result; - }, Object.create(null)); -} - /** * Default merge callback * - * @param {object} result The final resulting object - * @param {PropertyKey} key - * @param {any} value - * @param {object} source - * @param {number} sourceIndex + * @param {object} result The resulting object (relative to object depth) + * @param {PropertyKey} key Property Key in source object + * @param {any} value Value of the property in source object + * @param {object} source The source object that holds the property + * @param {number} sourceIndex Source index (relative to object depth) * @param {MergeOptions} options * * @returns {any} The value to be merged into the resulting object @@ -218,3 +179,68 @@ export const defaultMergeCallback: MergeCallback = function( }); } +/** + * Performs merge of given objects + * + * @internal + * + * @param {object[]} sources + * @param {MergeOptions} options + * + * @returns {object} + * + * @throws {MergeError} If unable to merge objects + */ +function performMerge(sources: object[], options: MergeOptions): object +{ + return sources.reduce((result: object, source: object, index: number) => { + if (Array.isArray(source)) { + throw new MergeError(`Unable to merge object with an array source, (source index: ${index})`, { + cause: { + source: source, + index: index + } + }); + } + + const keys: PropertyKey[] = Reflect.ownKeys(source); + for (const key of keys){ + // Skip key if needed ... + if ((options.skip as SkipKeyCallback)(key, source, result)) { + continue; + } + + // Resolve the value via callback and set it in resulting object. + result[key] = options.callback( + result, + key, + source[key], + source, + index, + options + ); + } + + return result; + }, Object.create(null)); +} + +/** + * Returns a default "skip" callback, for given property keys + * + * @internal + * + * @param {PropertyKey[]} keys Properties that must not be merged + * + * @return {SkipKeyCallback} + */ +function makeDefaultSkipCallback(keys: PropertyKey[]): SkipKeyCallback +{ + return ( + key: PropertyKey, + source: object, + result: object /* eslint-disable-line @typescript-eslint/no-unused-vars */ + ) => { + return keys.includes(key) && Reflect.has(source, key); + } +} From 049aed3d491a4fa2d6d3cc6b41a314e92db7a6e1 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 09:05:17 +0100 Subject: [PATCH 147/424] Add maximum merge depth handling This should prevent the merge method from going in an infinite loop. --- .../src/support/objects/MergeOptions.ts | 12 ++++++ .../contracts/src/support/objects/index.ts | 7 ++++ .../contracts/src/support/objects/types.ts | 3 +- packages/support/src/objects/merge.ts | 38 ++++++++++++++++--- 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/packages/contracts/src/support/objects/MergeOptions.ts b/packages/contracts/src/support/objects/MergeOptions.ts index d49800fd..56223629 100644 --- a/packages/contracts/src/support/objects/MergeOptions.ts +++ b/packages/contracts/src/support/objects/MergeOptions.ts @@ -5,6 +5,18 @@ import type { MergeCallback, SkipKeyCallback } from "./types"; */ export default interface MergeOptions { + /** + * The maximum merge depth + * + * **Note**: _Value must be greater than or equal zero._ + * + * **Note**: _Defaults to [DEFAULT_MAX_MERGE_DEPTH]{@link import('@aedart/contracts/support/objects').DEFAULT_MAX_MERGE_DEPTH} + * when not specified._ + * + * @type {number} + */ + depth?: number; + /** * Property Keys that must not be merged. * diff --git a/packages/contracts/src/support/objects/index.ts b/packages/contracts/src/support/objects/index.ts index e7650987..5f606a9c 100644 --- a/packages/contracts/src/support/objects/index.ts +++ b/packages/contracts/src/support/objects/index.ts @@ -12,6 +12,13 @@ export const SUPPORT_OBJECTS: unique symbol = Symbol('@aedart/contracts/support/ */ export const DEFAULT_MERGE_SKIP_KEYS: PropertyKey[] = [ 'prototype', '__proto__' ]; +/** + * Default maximum merge depth + * + * @type {number} + */ +export const DEFAULT_MAX_MERGE_DEPTH: number = 512; + import MergeOptions from "./MergeOptions"; export { type MergeOptions diff --git a/packages/contracts/src/support/objects/types.ts b/packages/contracts/src/support/objects/types.ts index f45de7b4..0f71c90c 100644 --- a/packages/contracts/src/support/objects/types.ts +++ b/packages/contracts/src/support/objects/types.ts @@ -8,7 +8,7 @@ import MergeOptions from "./MergeOptions"; * **Note**: _[Skipped keys]{@link MergeOptions.skip} are NOT provided to the callback._ * * **Note**: _The callback is responsible for respecting the given [options]{@link MergeOptions}, - * ([keys to be skipped]{@link MergeOptions.skip} and max depth reach excluded)_ + * ([keys to be skipped]{@link MergeOptions.skip} and [depth]{@link MergeOptions.depth} excluded)_ */ export type MergeCallback = ( result: object, @@ -16,6 +16,7 @@ export type MergeCallback = ( value: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ source: object, sourceIndex: number, + depth: number, options: MergeOptions ) => any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index 9716981d..3a5b14b3 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -3,7 +3,10 @@ import type { MergeCallback, SkipKeyCallback } from "@aedart/contracts/support/objects"; -import { DEFAULT_MERGE_SKIP_KEYS } from "@aedart/contracts/support/objects"; +import { + DEFAULT_MAX_MERGE_DEPTH, + DEFAULT_MERGE_SKIP_KEYS +} from "@aedart/contracts/support/objects"; import { isPrimitive, descTag } from "@aedart/support/misc"; import MergeError from "./exceptions/MergeError"; @@ -13,6 +16,7 @@ import MergeError from "./exceptions/MergeError"; * @type {MergeOptions} */ export const DEFAULT_MERGE_OPTIONS: MergeOptions = { + depth: DEFAULT_MAX_MERGE_DEPTH, skip: DEFAULT_MERGE_SKIP_KEYS, overwriteWithUndefined: true, mergeArrays: false, @@ -55,6 +59,15 @@ export function merge( ...userOptions }; + // Abort in case of invalid maximum depth + if (typeof resolved.depth != 'number' || resolved.depth < 0) { + throw new MergeError('Invalid maximum "depth" merge option value', { + cause: { + options: resolved + } + }); + } + // Resolve the skip callback. if (Array.isArray(resolved.skip)) { resolved.skip = makeDefaultSkipCallback(resolved.skip); @@ -89,6 +102,7 @@ export function merge( * @param {any} value Value of the property in source object * @param {object} source The source object that holds the property * @param {number} sourceIndex Source index (relative to object depth) + * @param {number} depth Current depth * @param {MergeOptions} options * * @returns {any} The value to be merged into the resulting object @@ -101,6 +115,7 @@ export const defaultMergeCallback: MergeCallback = function( value: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ source: object, sourceIndex: number, + depth: number, options: MergeOptions ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { @@ -160,11 +175,11 @@ export const defaultMergeCallback: MergeCallback = function( if (type == 'object') { // Merge with existing, if not null... if (exists && typeof result[key] == 'object' && result[key] !== null) { - return performMerge([ result[key], value ], options); + return performMerge([ result[key], value ], options, depth + 1); } // Otherwise, create a new object and merge it. - return performMerge([ {}, value ], options); + return performMerge([ {}, value ], options, depth + 1); } // If for some reason this point is reached, it means that we are unable to merge "something". @@ -186,19 +201,31 @@ export const defaultMergeCallback: MergeCallback = function( * * @param {object[]} sources * @param {MergeOptions} options + * @param {number} [depth=0] * * @returns {object} * * @throws {MergeError} If unable to merge objects */ -function performMerge(sources: object[], options: MergeOptions): object +function performMerge(sources: object[], options: MergeOptions, depth: number = 0): object { + // Abort if maximum depth has been reached + if (depth > options.depth) { + throw new MergeError(`Maximum merge depth (${options.depth}) has been exceeded`, { + cause: { + source: sources, + depth: depth + } + }); + } + return sources.reduce((result: object, source: object, index: number) => { if (Array.isArray(source)) { throw new MergeError(`Unable to merge object with an array source, (source index: ${index})`, { cause: { source: source, - index: index + index: index, + depth: depth } }); } @@ -217,6 +244,7 @@ function performMerge(sources: object[], options: MergeOptions): object source[key], source, index, + depth, options ); } From 809db15cde5d0c90c83ab7e83e66f8a5981be1bc Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 09:26:26 +0100 Subject: [PATCH 148/424] Add examples for options --- .../src/support/objects/MergeOptions.ts | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/objects/MergeOptions.ts b/packages/contracts/src/support/objects/MergeOptions.ts index 56223629..823f28c5 100644 --- a/packages/contracts/src/support/objects/MergeOptions.ts +++ b/packages/contracts/src/support/objects/MergeOptions.ts @@ -26,6 +26,18 @@ export default interface MergeOptions * **Callback**: _A callback can be specified to determine if a given key, * in a source object should be skipped._ * + * **Example:** + * ```js + * const a { 'foo': true }; + * const a { 'bar': true, 'zar': true }; + * + * merge([ a, b ], { skip: [ 'zar' ] }); // { 'foo': true, 'bar': true } + * + * merge([ a, b ], { skip: (key, source) => { + * return key === 'bar' && Reflect.has(source, key); + * } }); // { 'foo': true, 'zar': true } + * ``` + * * @type {PropertyKey[] | SkipKeyCallback} */ skip?: PropertyKey[] | SkipKeyCallback; @@ -38,6 +50,16 @@ export default interface MergeOptions * * **When `false`**: _If an existing property value is not `undefined`, it will NOT be overwritten * with new value, if the new value is `undefined`._ + * + * **Example:** + * ```js + * const a { 'foo': true }; + * const a { 'foo': undefined }; + * + * merge([ a, b ]); // { 'foo': undefined } + * + * merge([ a, b ], { overwriteWithUndefined: false }); // { 'foo': true } + * ``` * * @type {boolean} */ @@ -49,6 +71,16 @@ export default interface MergeOptions * **When `true`**: _existing array property value is attempted merged with new array value._ * * **When `false` (_default behaviour_)**: _existing array property value is overwritten with new array value_ + * + * **Example:** + * ```js + * const a { 'foo': [ 1, 2, 3 ] }; + * const a { 'foo': [ 4, 5, 6 ] }; + * + * merge([ a, b ]); // { 'foo': [ 4, 5, 6 ] } + * + * merge([ a, b ], { mergeArrays: true }); // { 'foo': [ 1, 2, 3, 4, 5, 6 ] } + * ``` * * @type {boolean} */ @@ -57,8 +89,8 @@ export default interface MergeOptions /** * The merge callback that must be applied * - * **Note**: _When no callback is provided, then a default is - * set by the merge function_ + * **Note**: _When no callback is provided, then the merge function's default + * callback is used._ * * @type {MergeCallback} */ From 71e882c8c54a4380b2e3d8a943b5366c7ae31de9 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 10:12:04 +0100 Subject: [PATCH 149/424] Refactor defaultMergeCallback(), use switch case Hopefully this will improve the performance. --- packages/support/src/objects/merge.ts | 181 +++++++++++++++++--------- 1 file changed, 122 insertions(+), 59 deletions(-) diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index 3a5b14b3..3fe5a943 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -7,7 +7,7 @@ import { DEFAULT_MAX_MERGE_DEPTH, DEFAULT_MERGE_SKIP_KEYS } from "@aedart/contracts/support/objects"; -import { isPrimitive, descTag } from "@aedart/support/misc"; +import { descTag } from "@aedart/support/misc"; import MergeError from "./exceptions/MergeError"; /** @@ -121,77 +121,84 @@ export const defaultMergeCallback: MergeCallback = function( { // Determine if an existing property exists, and its type... const exists: boolean = Reflect.has(result, key); - const type: string = typeof value; - - // Primitives - // @see https://developer.mozilla.org/en-US/docs/Glossary/Primitive - if (isPrimitive(value)) { - - // Do not overwrite existing value with `undefined`, if options do not allow it... - if (value === undefined && options.overwriteWithUndefined === false && exists && result[key] !== undefined) { - return result[key]; - } - - // Otherwise, just return the value for primitive value. - return value; - } + const type: string = typeof value; - // Arrays - if (Array.isArray(value)) { - - // Create a structured clone of array value. However, this can fail if array contains - // complex values, e.g. functions, objects, ...etc. - try { - const clonedArray = structuredClone(value); + switch (type) { + + // -------------------------------------------------------------------------------------------------------- // + // Primitives + // @see https://developer.mozilla.org/en-US/docs/Glossary/Primitive + + case 'undefined': + // Do not overwrite existing value with `undefined`, if options do not allow it... + if (value === undefined + && options.overwriteWithUndefined === false + && exists + && result[key] !== undefined + ) { + return result[key]; + } + + return value; + + case 'string': + case 'number': + case 'bigint': + case 'boolean': + case 'symbol': + return value; + + // -------------------------------------------------------------------------------------------------------- // + // Functions + + case 'function': + return value; - // Merge array values if required. - if (options.mergeArrays === true && exists && Array.isArray(result[key])) { - return [ ...result[key], ...clonedArray ]; + // -------------------------------------------------------------------------------------------------------- // + // Null, Arrays and Objects + case 'object': + // Null (primitive) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + if (value === null) { + return value; } - // Returns the cloned array, to avoid unintended manipulation... - return clonedArray; - } catch (error) { - throw new MergeError(`Unable to merge array value at source index ${sourceIndex}`, { + // Arrays - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + if (Array.isArray(value)) { + return resolveArrayValue( + result, + key, + value, + source, + sourceIndex, + depth, + options + ); + } + // TODO: Array-Like objects ??? + + // Objects - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Merge with existing, if existing value is not null... + if (exists && typeof result[key] == 'object' && result[key] !== null) { + return performMerge([ result[key], value ], options, depth + 1); + } + + // Otherwise, create a new object and merge it. + return performMerge([ Object.create(null), value ], options, depth + 1); + + // -------------------------------------------------------------------------------------------------------- // + // If for some reason this point is reached, it means that we are unable to merge "something". + default: + throw new MergeError(`Unable to merge value of type ${type} (${descTag(value)}) at source index ${sourceIndex}`, { cause: { - error: error, key: key, value: value, source: source, sourceIndex: sourceIndex, + depth: depth, options: options } }); - } } - // TODO: Array-Like objects ??? - - // Functions are an exception to the copy rule. They must remain untouched... - if (type == 'function') { - return value; - } - - // Objects - if (type == 'object') { - // Merge with existing, if not null... - if (exists && typeof result[key] == 'object' && result[key] !== null) { - return performMerge([ result[key], value ], options, depth + 1); - } - - // Otherwise, create a new object and merge it. - return performMerge([ {}, value ], options, depth + 1); - } - - // If for some reason this point is reached, it means that we are unable to merge "something". - throw new MergeError(`Unable to merge value of type ${type} (${descTag(value)}) at source index ${sourceIndex}`, { - cause: { - key: key, - value: value, - source: source, - sourceIndex: sourceIndex, - options: options - } - }); } /** @@ -253,6 +260,62 @@ function performMerge(sources: object[], options: MergeOptions, depth: number = }, Object.create(null)); } +/** + * Resolves array value + * + * @param {object} result + * @param {PropertyKey} key + * @param {any[]} value + * @param {object} source + * @param {number} sourceIndex + * @param {number} depth + * @param {MergeOptions} options + * + * @return {any[]} + * + * @throws {MergeError} + */ +function resolveArrayValue( + result: object, + key: PropertyKey, + value: any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + source: object, + sourceIndex: number, + depth: number, + options: MergeOptions +): any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ +{ + try { + // Create a structured clone of the array value, to avoid unintended manipulation of the + // original array. However, if the array contains complex values, e.g. functions, then + // this can fail. + const clonedArray = structuredClone(value); + + // Merge array values if required. + if (options.mergeArrays === true + && Reflect.has(result, key) + && Array.isArray(result[key]) + ) { + return [ ...result[key], ...clonedArray ]; + } + + // Return the clone array... + return clonedArray; + } catch (error) { + throw new MergeError(`Unable to merge array value at source index ${sourceIndex}`, { + cause: { + error: error, + key: key, + value: value, + source: source, + sourceIndex: sourceIndex, + depth: depth, + options: options + } + }); + } +} + /** * Returns a default "skip" callback, for given property keys * From e387a656cabd7c8a3e522c7e1bdb0a36610c5e98 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 10:13:12 +0100 Subject: [PATCH 150/424] Mark resolveArrayValue() as internal --- packages/support/src/objects/merge.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index 3fe5a943..9df1cbc7 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -263,6 +263,8 @@ function performMerge(sources: object[], options: MergeOptions, depth: number = /** * Resolves array value * + * @internal + * * @param {object} result * @param {PropertyKey} key * @param {any[]} value From afaa2f386aaa43c5dc3782583ea3073b98366067 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 10:31:09 +0100 Subject: [PATCH 151/424] Remove redundant exists const --- packages/support/src/objects/merge.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index 9df1cbc7..11f3bf61 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -119,8 +119,7 @@ export const defaultMergeCallback: MergeCallback = function( options: MergeOptions ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { - // Determine if an existing property exists, and its type... - const exists: boolean = Reflect.has(result, key); + // Determine the type and resolve value based on it... const type: string = typeof value; switch (type) { @@ -133,7 +132,7 @@ export const defaultMergeCallback: MergeCallback = function( // Do not overwrite existing value with `undefined`, if options do not allow it... if (value === undefined && options.overwriteWithUndefined === false - && exists + && Reflect.has(result, key) && result[key] !== undefined ) { return result[key]; @@ -178,7 +177,7 @@ export const defaultMergeCallback: MergeCallback = function( // Objects - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Merge with existing, if existing value is not null... - if (exists && typeof result[key] == 'object' && result[key] !== null) { + if (Reflect.has(result, key) && typeof result[key] == 'object' && result[key] !== null) { return performMerge([ result[key], value ], options, depth + 1); } From da92724bf711b5c6b2d9a3b84eaddfd11172ab1b Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 10:32:03 +0100 Subject: [PATCH 152/424] Add additional tests for custom callback, skip callback and max depth --- .../packages/support/objects/merge.test.js | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js index cb550f84..77cee0fa 100644 --- a/tests/browser/packages/support/objects/merge.test.js +++ b/tests/browser/packages/support/objects/merge.test.js @@ -94,6 +94,41 @@ describe('@aedart/support/objects', () => { .toBe(a['foo']) }); + it('can apply custom merge callback', () => { + const a = { + 'a': 1 + }; + const b = { + 'b': 2 + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ], (result, key, value) => { + if (key === 'b') { + return value + 1; + } + + return value; + }); + + // --------------------------------------------------------------------- // + + expect(Reflect.has(result, 'a')) + .withContext('a missing') + .toBeTrue(); + expect(result['a']) + .withContext('Merge callback not applied for a') + .toBe(a['a']); + + expect(Reflect.has(result, 'b')) + .withContext('b missing') + .toBeTrue() + expect(result['b']) + .withContext('Merge callback not applied for b') + .toBe(b['b'] + 1); + }); + it('skips default keys', () => { const a = { 'foo': 'bar' @@ -142,6 +177,38 @@ describe('@aedart/support/objects', () => { .withContext('Skipped key is in output') .toBeFalse(); }); + + it('can skip keys via callback', () => { + const a = { + 'foo': 'bar' + }; + const b = { + 'bar': 'foo', + 'ab': 'ba' + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ], { + skip: (key, source) => { + return key === 'ab' && Reflect.has(source, key); + } + }); + + // Debug + // console.log('result', result); + + expect(Reflect.has(result, 'foo')) + .withContext('Incorrect key (foo) skipped') + .toBeTrue(); + expect(Reflect.has(result, 'bar')) + .withContext('Incorrect key (bar) skipped') + .toBeTrue(); + + expect(Reflect.has(result, 'ab')) + .withContext('Skipped key is in output') + .toBeFalse(); + }); it('can merge values of symbol keys', () => { @@ -308,5 +375,74 @@ describe('@aedart/support/objects', () => { .withContext('Incorrect merge of nested objects') .toBe(expected) }); + + it('fails when invalid maximum depth option provided', () => { + + const a = { + 'foo': 'bar', + }; + const b = { + 'foo': true + }; + + // --------------------------------------------------------------------- // + + const callback = () => { + return merge([ a, b ], { depth: -1 }); + } + + // --------------------------------------------------------------------- // + + expect(callback) + .toThrowError(MergeError); + }); + + it('fails when maximum depth has been exceeded', () => { + + const a = { + 'person': { + 'name': 'Risk' + } + }; + const b = { + 'person': { + 'age': 31, + 'address': { + 'street': 'Somewhere Str. 654' // This depth level should cause failure... + } + } + }; + + // --------------------------------------------------------------------- // + + const callback = () => { + return merge([ a, b ], { depth: 1 }); + } + + // --------------------------------------------------------------------- // + + expect(callback) + .toThrowError(MergeError); + }); + + it('can merge with maximum depth set to zero', () => { + + const a = { + 'foo': false, + }; + const b = { + 'foo': true, + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ], { depth: 0 }); + + // --------------------------------------------------------------------- // + + expect(Reflect.has(result, 'foo') && result['foo'] === true) + .withContext('Failed to merge with maximum depth option set to zero') + .toBeTrue() + }); }); }); \ No newline at end of file From 36fff4807ec475eed29331844da61e586766b723 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 10:58:49 +0100 Subject: [PATCH 153/424] Cleanup --- packages/support/src/objects/merge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index 11f3bf61..aaea8d1b 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -121,7 +121,7 @@ export const defaultMergeCallback: MergeCallback = function( { // Determine the type and resolve value based on it... const type: string = typeof value; - + switch (type) { // -------------------------------------------------------------------------------------------------------- // From b541f89b793f2c199f90d8bb1bc630c4048e4329 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 11:04:13 +0100 Subject: [PATCH 154/424] Init support arrays submodules --- aliases.js | 1 + packages/contracts/package.json | 5 +++++ packages/contracts/rollup.config.mjs | 1 + packages/contracts/src/support/arrays/index.ts | 6 ++++++ packages/support/package.json | 5 +++++ packages/support/rollup.config.mjs | 1 + packages/support/src/arrays/index.ts | 2 ++ 7 files changed, 21 insertions(+) create mode 100644 packages/contracts/src/support/arrays/index.ts create mode 100644 packages/support/src/arrays/index.ts diff --git a/aliases.js b/aliases.js index b5198548..b5f9cc7b 100644 --- a/aliases.js +++ b/aliases.js @@ -19,6 +19,7 @@ module.exports = { alias: { // contracts + '@aedart/contracts/support/arrays': path.resolve(__dirname, './packages/contracts/support/arrays'), '@aedart/contracts/support/concerns': path.resolve(__dirname, './packages/contracts/support/concerns'), '@aedart/contracts/support/exceptions': path.resolve(__dirname, './packages/contracts/support/exceptions'), '@aedart/contracts/support/meta': path.resolve(__dirname, './packages/contracts/support/meta'), diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 1ecc1d67..14d2d884 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -36,6 +36,11 @@ "import": "./dist/esm/support.js", "require": "./dist/cjs/support.cjs" }, + "./support/arrays": { + "types": "./dist/types/support/arrays.d.ts", + "import": "./dist/esm/support/arrays.js", + "require": "./dist/cjs/support/arrays.cjs" + }, "./support/concerns": { "types": "./dist/types/support/concerns.d.ts", "import": "./dist/esm/support/concerns.js", diff --git a/packages/contracts/rollup.config.mjs b/packages/contracts/rollup.config.mjs index 9a56f5ce..066e3fd3 100644 --- a/packages/contracts/rollup.config.mjs +++ b/packages/contracts/rollup.config.mjs @@ -4,6 +4,7 @@ export default createConfig({ baseDir: new URL('.', import.meta.url), external: [ '@aedart/contracts/support', + '@aedart/contracts/support/arrays', '@aedart/contracts/support/concerns', '@aedart/contracts/support/exceptions', '@aedart/contracts/support/meta', diff --git a/packages/contracts/src/support/arrays/index.ts b/packages/contracts/src/support/arrays/index.ts new file mode 100644 index 00000000..63b35b0c --- /dev/null +++ b/packages/contracts/src/support/arrays/index.ts @@ -0,0 +1,6 @@ +/** + * Support Arrays identifier + * + * @type {Symbol} + */ +export const SUPPORT_ARRAYS: unique symbol = Symbol('@aedart/contracts/support/arrays'); \ No newline at end of file diff --git a/packages/support/package.json b/packages/support/package.json index 56283270..f48dcf45 100644 --- a/packages/support/package.json +++ b/packages/support/package.json @@ -28,6 +28,11 @@ "import": "./dist/esm/support.js", "require": "./dist/cjs/support.cjs" }, + "./arrays": { + "types": "./dist/types/arrays.d.ts", + "import": "./dist/esm/arrays.js", + "require": "./dist/cjs/arrays.cjs" + }, "./concerns": { "types": "./dist/types/concerns.d.ts", "import": "./dist/esm/concerns.js", diff --git a/packages/support/rollup.config.mjs b/packages/support/rollup.config.mjs index cc3b6175..299cbe5b 100644 --- a/packages/support/rollup.config.mjs +++ b/packages/support/rollup.config.mjs @@ -5,6 +5,7 @@ export default createConfig({ external: [ '@aedart/contracts', '@aedart/contracts/support', + '@aedart/contracts/support/arrays', '@aedart/contracts/support/concerns', '@aedart/contracts/support/exceptions', '@aedart/contracts/support/meta', diff --git a/packages/support/src/arrays/index.ts b/packages/support/src/arrays/index.ts new file mode 100644 index 00000000..a274652d --- /dev/null +++ b/packages/support/src/arrays/index.ts @@ -0,0 +1,2 @@ +// TODO: replace this... +export const TMP: string = 'TODO'; \ No newline at end of file From a13947b64e0457374524be944f02e85c20d7334d Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 11:04:51 +0100 Subject: [PATCH 155/424] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0efca248..78ca9eed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `@aedart/contracts/support/exceptions` and `@aedart/support/exceptions` submodules. * `@aedart/contracts/support/objects` submodule. +* `@aedart/contracts/support/arrays` and `@aedart/support/arrays` submodules. * `Throwable` (_extends TypeScript's `Error` interface_) interface in `@aedart/contracts/support/exceptions`. * `LogicalError` and `AbstractClassError` exceptions in `@aedart/support/exceptions`. * `FUNCTION_PROTOTYPE` const in `@aedart/contracts/support/reflections`. From 494143e9b2524894ad2f88a4c5d1ea29354a0498 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 11:20:32 +0100 Subject: [PATCH 156/424] Add isTypedArray() util function --- packages/support/src/arrays/index.ts | 3 +- packages/support/src/arrays/isTypedArray.ts | 13 ++++++++ .../support/arrays/isTypedArray.test.js | 31 +++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 packages/support/src/arrays/isTypedArray.ts create mode 100644 tests/browser/packages/support/arrays/isTypedArray.test.js diff --git a/packages/support/src/arrays/index.ts b/packages/support/src/arrays/index.ts index a274652d..d43e2e55 100644 --- a/packages/support/src/arrays/index.ts +++ b/packages/support/src/arrays/index.ts @@ -1,2 +1 @@ -// TODO: replace this... -export const TMP: string = 'TODO'; \ No newline at end of file +export * from './isTypedArray'; \ No newline at end of file diff --git a/packages/support/src/arrays/isTypedArray.ts b/packages/support/src/arrays/isTypedArray.ts new file mode 100644 index 00000000..d813a3a1 --- /dev/null +++ b/packages/support/src/arrays/isTypedArray.ts @@ -0,0 +1,13 @@ +/** + * Determine if given target is an instance of a `TypedArray` + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray + * + * @param {object} target + * + * @return {boolean} + */ +export function isTypedArray(target: object): boolean +{ + return target instanceof Reflect.getPrototypeOf(Int8Array); +} \ No newline at end of file diff --git a/tests/browser/packages/support/arrays/isTypedArray.test.js b/tests/browser/packages/support/arrays/isTypedArray.test.js new file mode 100644 index 00000000..8dc18bbc --- /dev/null +++ b/tests/browser/packages/support/arrays/isTypedArray.test.js @@ -0,0 +1,31 @@ +import { isTypedArray } from "@aedart/support/arrays"; + +describe('@aedart/support/arrays', () => { + describe('isTypedArray()', () => { + + it('can determine if object is Typed Array', () => { + const dataSet = [ + { value: [], expected: false, name: 'Array' }, + { value: new Map(), expected: false, name: 'Map' }, + + { value: new Int8Array(), expected: true, name: 'Int8Array' }, + { value: new Uint8Array(), expected: true, name: 'Uint8Array' }, + { value: new Uint8ClampedArray(), expected: true, name: 'Uint8ClampedArray' }, + { value: new Int16Array(), expected: true, name: 'Int16Array' }, + { value: new Uint16Array(), expected: true, name: 'Uint16Array' }, + { value: new Int32Array(), expected: true, name: 'Int32Array' }, + { value: new Uint32Array(), expected: true, name: 'Uint32Array' }, + { value: new Float32Array(), expected: true, name: 'Float32Array' }, + { value: new Float64Array(), expected: true, name: 'Float64Array' }, + { value: new BigInt64Array(), expected: true, name: 'BigInt64Array' }, + { value: new BigUint64Array(), expected: true, name: 'BigUint64Array' }, + ]; + + for (const data of dataSet) { + expect(isTypedArray(data.value)) + .withContext(`${name} was expected to ${data.expected.toString()}`) + .toBe(data.expected); + } + }); + }); +}); \ No newline at end of file From 9491d51a3405ea45e092fd3dd6a632d59c64c4a1 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 11:20:38 +0100 Subject: [PATCH 157/424] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ca9eed..436fb5a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `isSubclass()` in `@aedart/support/reflections`. * `classOwnKeys()` in `@aedart/support/reflections`. * `merge()` in `@aedart/support/objects`. +* `isTypedArray()` in `@aedart/support/arrays`. ## [0.8.0] - 2024-02-12 From bf3e54bc54c710d5fb238c6ff445407af7c2c2a2 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 12:09:06 +0100 Subject: [PATCH 158/424] Add array merge() util function --- .../src/arrays/exceptions/ArrayMergeError.ts | 28 ++++++++ .../support/src/arrays/exceptions/index.ts | 4 ++ packages/support/src/arrays/index.ts | 5 +- packages/support/src/arrays/merge.ts | 36 +++++++++++ .../packages/support/arrays/merge.test.js | 64 +++++++++++++++++++ 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 packages/support/src/arrays/exceptions/ArrayMergeError.ts create mode 100644 packages/support/src/arrays/exceptions/index.ts create mode 100644 packages/support/src/arrays/merge.ts create mode 100644 tests/browser/packages/support/arrays/merge.test.js diff --git a/packages/support/src/arrays/exceptions/ArrayMergeError.ts b/packages/support/src/arrays/exceptions/ArrayMergeError.ts new file mode 100644 index 00000000..86b75fa9 --- /dev/null +++ b/packages/support/src/arrays/exceptions/ArrayMergeError.ts @@ -0,0 +1,28 @@ +import type { Throwable } from "@aedart/contracts/support/exceptions"; + +/** + * Array Merge Error + * + * To be thrown when two or more arrays are unable to be merged. + */ +export default class ArrayMergeError extends Error implements Throwable +{ + /** + * Create a new Array Merge Error instance + * + * @param {string} [message] + * @param {ErrorOptions} [options] + */ + constructor(message?: string, options?: ErrorOptions) + { + super(message, options); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ArrayMergeError); + } else { + this.stack = (new Error()).stack; + } + + this.name = "ArrayMergeError"; + } +} \ No newline at end of file diff --git a/packages/support/src/arrays/exceptions/index.ts b/packages/support/src/arrays/exceptions/index.ts new file mode 100644 index 00000000..f41270c5 --- /dev/null +++ b/packages/support/src/arrays/exceptions/index.ts @@ -0,0 +1,4 @@ +import ArrayMergeError from "./ArrayMergeError"; +export { + ArrayMergeError +} \ No newline at end of file diff --git a/packages/support/src/arrays/index.ts b/packages/support/src/arrays/index.ts index d43e2e55..8e1056ee 100644 --- a/packages/support/src/arrays/index.ts +++ b/packages/support/src/arrays/index.ts @@ -1 +1,4 @@ -export * from './isTypedArray'; \ No newline at end of file +export * from './isTypedArray'; +export * from './merge'; + +export * from './exceptions'; \ No newline at end of file diff --git a/packages/support/src/arrays/merge.ts b/packages/support/src/arrays/merge.ts new file mode 100644 index 00000000..cf308667 --- /dev/null +++ b/packages/support/src/arrays/merge.ts @@ -0,0 +1,36 @@ +import { ArrayMergeError } from "./exceptions"; + +/** + * Merge two or more arrays + * + * **Note**: _Method attempts to deep copy array values, via [structuredClone]{@link https://developer.mozilla.org/en-US/docs/Web/API/structuredClone}_ + * + * @param {...any[]} sources + * + * @return {any[]} + * + * @throws {ArrayMergeError} If unable to merge arrays, e.g. if a value cannot be cloned via `structuredClone()` + */ +export function merge( + ...sources: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ +): any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ +{ + try { + // Array.concat only performs shallow copies of the array values, which might + // fine in some situations. However, this version must ensure to perform a + // deep copy of the values... + + return structuredClone([].concat(...sources)); + } catch (e) { + const reason: string = (typeof e == 'object' && Reflect.has(e, 'message')) + ? e.message + : 'unknown reason'; + + throw new ArrayMergeError('Unable to merge arrays: ' + reason, { + cause: { + previous: e, + sources: sources + } + }); + } +} \ No newline at end of file diff --git a/tests/browser/packages/support/arrays/merge.test.js b/tests/browser/packages/support/arrays/merge.test.js new file mode 100644 index 00000000..c25f5712 --- /dev/null +++ b/tests/browser/packages/support/arrays/merge.test.js @@ -0,0 +1,64 @@ +import { ArrayMergeError, merge } from "@aedart/support/arrays"; + +describe('@aedart/support/arrays', () => { + describe('merge()', () => { + + it('can merge multiple arrays', () => { + + const a = [ 1, 2, 3 ]; + const b = [ 4, 5, 6 ]; + const c = [ 7, 8, 9 ]; + + // --------------------------------------------------------------- // + + const result = merge(a, b, c); + + // Debug + // console.log('result', result); + + expect(result) + .toEqual([ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]); + }); + + it('does not shallow copy simple object values', () => { + + const objA = { foo: true }; + const objB = { bar: true }; + + const a = [ objA ]; + const b = [ objB ]; + + // --------------------------------------------------------------- // + + const result = merge(a, b); + + // Debug + // console.log('result', result); + + expect(result[0]) + .withContext('Object a is a shallow copy!') + .not + .toBe(objA); + + expect(result[1]) + .withContext('Object b is a shallow copy!') + .not + .toBe(objB); + }); + + it('fails when attempting to merge arrays with non-cloneable values', () => { + + const a = [ 1, 2, 3 ]; + const b = [ function() {} ]; + + // --------------------------------------------------------------- // + + const callback = () => { + merge(a, b); + } + + expect(callback) + .toThrowError(ArrayMergeError); + }); + }); +}); \ No newline at end of file From c8cd615fba76bfcdbd4dabfdc29df6ff61a59b6c Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 12:09:15 +0100 Subject: [PATCH 159/424] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 436fb5a8..961f1e2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `isSubclass()` in `@aedart/support/reflections`. * `classOwnKeys()` in `@aedart/support/reflections`. * `merge()` in `@aedart/support/objects`. +* `merge()` in `@aedart/support/arrays`. * `isTypedArray()` in `@aedart/support/arrays`. ## [0.8.0] - 2024-02-12 From 323573878b1782d5c33a7b1e08a2b6d4dd8d93e7 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 12:32:44 +0100 Subject: [PATCH 160/424] Add getErrorMessage() util function --- .../support/src/exceptions/getErrorMessage.ts | 14 ++++++++++ packages/support/src/exceptions/index.ts | 4 ++- .../exceptions/getErrorMessage.test.js | 28 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 packages/support/src/exceptions/getErrorMessage.ts create mode 100644 tests/browser/packages/support/exceptions/getErrorMessage.test.js diff --git a/packages/support/src/exceptions/getErrorMessage.ts b/packages/support/src/exceptions/getErrorMessage.ts new file mode 100644 index 00000000..7152fde7 --- /dev/null +++ b/packages/support/src/exceptions/getErrorMessage.ts @@ -0,0 +1,14 @@ +/** + * Returns error message from {@link Error}, if possible + * + * @param {unknown} error Error or value that was thrown + * @param {string} [defaultMessage] A default message to return if unable to resolve error message + * + * @return {string} + */ +export function getErrorMessage(error: unknown, defaultMessage: string = 'unknown reason'): string +{ + return (typeof error == 'object' && Reflect.has(error, 'message')) + ? error.message + : defaultMessage; +} \ No newline at end of file diff --git a/packages/support/src/exceptions/index.ts b/packages/support/src/exceptions/index.ts index ccd1f3e2..daee4a19 100644 --- a/packages/support/src/exceptions/index.ts +++ b/packages/support/src/exceptions/index.ts @@ -3,4 +3,6 @@ import LogicalError from "./LogicalError"; export { AbstractClassError, LogicalError -} \ No newline at end of file +} + +export * from './getErrorMessage'; \ No newline at end of file diff --git a/tests/browser/packages/support/exceptions/getErrorMessage.test.js b/tests/browser/packages/support/exceptions/getErrorMessage.test.js new file mode 100644 index 00000000..9a94e44d --- /dev/null +++ b/tests/browser/packages/support/exceptions/getErrorMessage.test.js @@ -0,0 +1,28 @@ +import { getErrorMessage } from "@aedart/support/exceptions"; + +describe('@aedart/support/exceptions', () => { + + describe('getErrorMessage()', () => { + + it('returns error message', () => { + + const err = new Error('Lorum lipsum'); + + const result = getErrorMessage(err); + expect(result) + .toBe(err.message); + }); + + it('returns default message, when unable to resolve message from error', () => { + + const defaultMessage = 'My other failure reason'; + + const err = {}; + + const result = getErrorMessage(err, defaultMessage); + expect(result) + .toBe(defaultMessage); + }); + }); + +}); \ No newline at end of file From 4973dbe8e97a7795a988a8cc3d24d340c5d45c08 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 12:33:02 +0100 Subject: [PATCH 161/424] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 961f1e2e..a2d8c6d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `@aedart/contracts/support/arrays` and `@aedart/support/arrays` submodules. * `Throwable` (_extends TypeScript's `Error` interface_) interface in `@aedart/contracts/support/exceptions`. * `LogicalError` and `AbstractClassError` exceptions in `@aedart/support/exceptions`. +* `getErrorMessage()` in `@aedart/support/exceptions`. * `FUNCTION_PROTOTYPE` const in `@aedart/contracts/support/reflections`. * `hasPrototypeProperty()` and `assertHasPrototypeProperty()` in `@aedart/support/reflections`. * `getParentOfClass()` and `getAllParentsOfClass()` in `@aedart/support/reflections`. From 0397a6110e4250c2c03a1b67471f15139209021b Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 12:36:32 +0100 Subject: [PATCH 162/424] Refactor, use new array merge() util method for dealing with array values --- packages/support/src/objects/merge.ts | 29 +++++++++++++++------------ 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index aaea8d1b..4822e5e4 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -8,6 +8,8 @@ import { DEFAULT_MERGE_SKIP_KEYS } from "@aedart/contracts/support/objects"; import { descTag } from "@aedart/support/misc"; +import { merge as mergeArrays } from "@aedart/support/arrays"; +import { getErrorMessage } from "@aedart/support/exceptions"; import MergeError from "./exceptions/MergeError"; /** @@ -83,10 +85,12 @@ export function merge( throw error; } - - throw new MergeError('Unable to merge objects', { + + const reason: string = getErrorMessage(error); + + throw new MergeError(`Unable to merge objects: ${reason}`, { cause: { - error: error, + previous: error, sources: sources, options: resolved } @@ -287,25 +291,24 @@ function resolveArrayValue( ): any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ { try { - // Create a structured clone of the array value, to avoid unintended manipulation of the - // original array. However, if the array contains complex values, e.g. functions, then - // this can fail. - const clonedArray = structuredClone(value); - + // Use cloned array values, to avoid unintended manipulation of the original array values (if contains objects). + // However, if the array contains non-cloneable values, then this can fail. + // Merge array values if required. if (options.mergeArrays === true && Reflect.has(result, key) && Array.isArray(result[key]) ) { - return [ ...result[key], ...clonedArray ]; + return mergeArrays(result[key], value); } - // Return the clone array... - return clonedArray; + return mergeArrays(value); } catch (error) { - throw new MergeError(`Unable to merge array value at source index ${sourceIndex}`, { + const reason: string = getErrorMessage(error); + + throw new MergeError(`Unable to merge array value at source index ${sourceIndex}: ${reason}`, { cause: { - error: error, + previous: error, key: key, value: value, source: source, From d3fe1f6b17312f3f6b118a21a26673bf44bc3c66 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 12:56:19 +0100 Subject: [PATCH 163/424] Simplify array merge logic There is no longer any need to have a separate function with try-catch block for dealing with arrays. --- packages/support/src/objects/merge.ts | 78 +++++---------------------- 1 file changed, 12 insertions(+), 66 deletions(-) diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index 4822e5e4..611e1f6f 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -167,15 +167,18 @@ export const defaultMergeCallback: MergeCallback = function( // Arrays - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if (Array.isArray(value)) { - return resolveArrayValue( - result, - key, - value, - source, - sourceIndex, - depth, - options - ); + // Use cloned array values, to avoid unintended manipulation of the original array values (if contains objects). + // However, if the array contains non-cloneable values, then this can fail. + + // Merge array values if required. + if (options.mergeArrays === true + && Reflect.has(result, key) + && Array.isArray(result[key]) + ) { + return mergeArrays(result[key], value); + } + + return mergeArrays(value); } // TODO: Array-Like objects ??? @@ -263,63 +266,6 @@ function performMerge(sources: object[], options: MergeOptions, depth: number = }, Object.create(null)); } -/** - * Resolves array value - * - * @internal - * - * @param {object} result - * @param {PropertyKey} key - * @param {any[]} value - * @param {object} source - * @param {number} sourceIndex - * @param {number} depth - * @param {MergeOptions} options - * - * @return {any[]} - * - * @throws {MergeError} - */ -function resolveArrayValue( - result: object, - key: PropertyKey, - value: any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ - source: object, - sourceIndex: number, - depth: number, - options: MergeOptions -): any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ -{ - try { - // Use cloned array values, to avoid unintended manipulation of the original array values (if contains objects). - // However, if the array contains non-cloneable values, then this can fail. - - // Merge array values if required. - if (options.mergeArrays === true - && Reflect.has(result, key) - && Array.isArray(result[key]) - ) { - return mergeArrays(result[key], value); - } - - return mergeArrays(value); - } catch (error) { - const reason: string = getErrorMessage(error); - - throw new MergeError(`Unable to merge array value at source index ${sourceIndex}: ${reason}`, { - cause: { - previous: error, - key: key, - value: value, - source: source, - sourceIndex: sourceIndex, - depth: depth, - options: options - } - }); - } -} - /** * Returns a default "skip" callback, for given property keys * From a313e55294594ce698cc0f66e9a658b36b70d25b Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 13:17:20 +0100 Subject: [PATCH 164/424] Add TypedArray prototype const --- packages/contracts/src/support/reflections/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/contracts/src/support/reflections/index.ts b/packages/contracts/src/support/reflections/index.ts index 97bc7664..b7c2dad3 100644 --- a/packages/contracts/src/support/reflections/index.ts +++ b/packages/contracts/src/support/reflections/index.ts @@ -15,3 +15,14 @@ export const SUPPORT_REFLECTIONS: unique symbol = Symbol('@aedart/contracts/supp * @type {object} */ export const FUNCTION_PROTOTYPE: object = Reflect.getPrototypeOf(Function); + +/** + * `TypedArray` prototype + * + * **Note**: _Prototype is obtained via `Reflect.getPrototypeOf(Int8Array)`_ + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray + * + * @type {object} + */ +export const TYPED_ARRAY_PROTOTYPE: object = Reflect.getPrototypeOf(Int8Array); From b958fd800fb7576f14349c71989bd8231b71380c Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 13:17:38 +0100 Subject: [PATCH 165/424] Use the TypedArray const for instance of check --- packages/support/src/arrays/isTypedArray.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/support/src/arrays/isTypedArray.ts b/packages/support/src/arrays/isTypedArray.ts index d813a3a1..2047983f 100644 --- a/packages/support/src/arrays/isTypedArray.ts +++ b/packages/support/src/arrays/isTypedArray.ts @@ -1,3 +1,5 @@ +import { TYPED_ARRAY_PROTOTYPE } from "@aedart/contracts/support/reflections"; + /** * Determine if given target is an instance of a `TypedArray` * @@ -9,5 +11,5 @@ */ export function isTypedArray(target: object): boolean { - return target instanceof Reflect.getPrototypeOf(Int8Array); + return target instanceof TYPED_ARRAY_PROTOTYPE; } \ No newline at end of file From a21c79ee9cbfdeac557fe23442cbfa6c9387e31f Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 13:17:45 +0100 Subject: [PATCH 166/424] Change release notes --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2d8c6d2..6537b41b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `Throwable` (_extends TypeScript's `Error` interface_) interface in `@aedart/contracts/support/exceptions`. * `LogicalError` and `AbstractClassError` exceptions in `@aedart/support/exceptions`. * `getErrorMessage()` in `@aedart/support/exceptions`. -* `FUNCTION_PROTOTYPE` const in `@aedart/contracts/support/reflections`. +* `FUNCTION_PROTOTYPE` and `TYPED_ARRAY_PROTOTYPE` constants in `@aedart/contracts/support/reflections`. * `hasPrototypeProperty()` and `assertHasPrototypeProperty()` in `@aedart/support/reflections`. * `getParentOfClass()` and `getAllParentsOfClass()` in `@aedart/support/reflections`. * `getClassPropertyDescriptor()` and `getClassPropertyDescriptors()` in `@aedart/support/reflections`. From 112fef0e3f0e37fe4cd4c5e38ffda00647cf1a3d Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 13:45:27 +0100 Subject: [PATCH 167/424] Ensure merge options are frozen after resolved --- .../contracts/src/support/objects/types.ts | 2 +- packages/support/src/objects/merge.ts | 96 ++++++++++++------- 2 files changed, 61 insertions(+), 37 deletions(-) diff --git a/packages/contracts/src/support/objects/types.ts b/packages/contracts/src/support/objects/types.ts index 0f71c90c..b21e90d9 100644 --- a/packages/contracts/src/support/objects/types.ts +++ b/packages/contracts/src/support/objects/types.ts @@ -17,7 +17,7 @@ export type MergeCallback = ( source: object, sourceIndex: number, depth: number, - options: MergeOptions + options: Readonly ) => any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ /** diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index 611e1f6f..9714a03a 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -7,9 +7,11 @@ import { DEFAULT_MAX_MERGE_DEPTH, DEFAULT_MERGE_SKIP_KEYS } from "@aedart/contracts/support/objects"; +import type { Constructor } from "@aedart/contracts"; import { descTag } from "@aedart/support/misc"; import { merge as mergeArrays } from "@aedart/support/arrays"; import { getErrorMessage } from "@aedart/support/exceptions"; +import { TYPED_ARRAY_PROTOTYPE } from "@aedart/contracts/support/reflections"; import MergeError from "./exceptions/MergeError"; /** @@ -42,39 +44,9 @@ export function merge( options?: MergeCallback | MergeOptions ): object { - // Resolve merge callback - const callback: MergeCallback = (typeof options == 'function') - ? options - : defaultMergeCallback; - - // Resolve user provided merge options - const userOptions: MergeOptions = (typeof options == 'object' && options !== null) - ? options - : Object.create(null); - - // Merge the default and user provided options... - const resolved: MergeOptions = { - ...DEFAULT_MERGE_OPTIONS, - ...{ - callback: callback - }, - ...userOptions - }; - - // Abort in case of invalid maximum depth - if (typeof resolved.depth != 'number' || resolved.depth < 0) { - throw new MergeError('Invalid maximum "depth" merge option value', { - cause: { - options: resolved - } - }); - } + // Resolve the merge options + const resolved: Readonly = resolveOptions(options); - // Resolve the skip callback. - if (Array.isArray(resolved.skip)) { - resolved.skip = makeDefaultSkipCallback(resolved.skip); - } - // Perform actual merge... try { return performMerge(sources, resolved); @@ -98,6 +70,7 @@ export function merge( } } + /** * Default merge callback * @@ -107,7 +80,7 @@ export function merge( * @param {object} source The source object that holds the property * @param {number} sourceIndex Source index (relative to object depth) * @param {number} depth Current depth - * @param {MergeOptions} options + * @param {Readonly} options * * @returns {any} The value to be merged into the resulting object * @@ -120,7 +93,7 @@ export const defaultMergeCallback: MergeCallback = function( source: object, sourceIndex: number, depth: number, - options: MergeOptions + options: Readonly ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { // Determine the type and resolve value based on it... @@ -213,14 +186,14 @@ export const defaultMergeCallback: MergeCallback = function( * @internal * * @param {object[]} sources - * @param {MergeOptions} options + * @param {Readonly} options * @param {number} [depth=0] * * @returns {object} * * @throws {MergeError} If unable to merge objects */ -function performMerge(sources: object[], options: MergeOptions, depth: number = 0): object +function performMerge(sources: object[], options: Readonly, depth: number = 0): object { // Abort if maximum depth has been reached if (depth > options.depth) { @@ -266,6 +239,57 @@ function performMerge(sources: object[], options: MergeOptions, depth: number = }, Object.create(null)); } +/** + * Resolve the merge options + * + * @internal + * + * @param {MergeCallback | MergeOptions} [options] + * + * @return {Readonly} + * + * @throws {MergeError} + */ +function resolveOptions(options?: MergeCallback | MergeOptions): Readonly +{ + // Resolve merge callback + const callback: MergeCallback = (typeof options == 'function') + ? options + : defaultMergeCallback; + + // Resolve user provided merge options + const userOptions: MergeOptions = (typeof options == 'object' && options !== null) + ? options + : Object.create(null); + + // Merge the default and user provided options... + const resolved: MergeOptions = { + ...DEFAULT_MERGE_OPTIONS, + ...{ + callback: callback + }, + ...userOptions + }; + + // Abort in case of invalid maximum depth + if (typeof resolved.depth != 'number' || resolved.depth < 0) { + throw new MergeError('Invalid maximum "depth" merge option value', { + cause: { + options: resolved + } + }); + } + + // Resolve the skip callback. + if (Array.isArray(resolved.skip)) { + resolved.skip = makeDefaultSkipCallback(resolved.skip); + } + + // Freeze the resolved options to avoid strange behaviour, if user provides + // custom merge callback and attempts to change the options... + return Object.freeze(resolved); +} + /** * Returns a default "skip" callback, for given property keys * From 4618c13371153cc49ee01bafa292f14aaf61f61e Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 15:03:11 +0100 Subject: [PATCH 168/424] Handle native objects that can be cloned via structedClone() function --- packages/support/src/objects/merge.ts | 52 +++++++- .../packages/support/objects/merge.test.js | 126 ++++++++++++++++++ 2 files changed, 176 insertions(+), 2 deletions(-) diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index 9714a03a..9f1db8ff 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -153,9 +153,18 @@ export const defaultMergeCallback: MergeCallback = function( return mergeArrays(value); } - // TODO: Array-Like objects ??? - // Objects - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Objects (of native kind) - - - - - - - - - - - - - - - - - - - - - - - - - + // Clone the object value, if possible. + if (canCloneObjectValue(value)) { + return structuredClone(value); + } + + // TODO: WeakMap ??? + // TODO: WeakSet ??? + // TODO: WeakRef ??? + + // Objects (basic)- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Merge with existing, if existing value is not null... if (Reflect.has(result, key) && typeof result[key] == 'object' && result[key] !== null) { return performMerge([ result[key], value ], options, depth + 1); @@ -239,6 +248,45 @@ function performMerge(sources: object[], options: Readonly, depth: }, Object.create(null)); } +/** + * Determine if an object value can be cloned via `structuredClone()` + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/structuredClone + * + * @internal + * + * @param {object} value + * + * @return {boolean} + */ +function canCloneObjectValue(value: object): boolean +{ + const supported: Constructor[] = [ + // Array, // Handled by array, with evt. array value merges + ArrayBuffer, + Boolean, + DataView, + Date, + Error, + Map, + Number, + // Object, // Handled by "basic" objects merging... + // (Primitive Types), // Also handled elsewhere... + RegExp, + Set, + String, + TYPED_ARRAY_PROTOTYPE + ]; + + for (const constructor of supported) { + if (value instanceof constructor) { + return true; + } + } + + return false; +} + /** * Resolve the merge options * diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js index 77cee0fa..5702c7f6 100644 --- a/tests/browser/packages/support/objects/merge.test.js +++ b/tests/browser/packages/support/objects/merge.test.js @@ -1,4 +1,5 @@ import { DEFAULT_MERGE_SKIP_KEYS } from "@aedart/contracts/support/objects"; +import { TYPED_ARRAY_PROTOTYPE } from "@aedart/contracts/support/reflections"; import { merge, MergeError @@ -444,5 +445,130 @@ describe('@aedart/support/objects', () => { .withContext('Failed to merge with maximum depth option set to zero') .toBeTrue() }); + + it('can clones objects of native kind', () => { + const dataSet = [ + { + name: 'ArrayBuffer', + source: { value: new ArrayBuffer(8) }, + expectedInstanceOf: ArrayBuffer, + match: (cloned) => { + return cloned.byteLength === 8; + } + }, + { + name: 'Boolean', + source: { value: new Boolean(true) }, + expectedInstanceOf: Boolean, + match: (cloned) => { + return cloned.valueOf() === true; + } + }, + { + name: 'DataView', + source: { value: new DataView(new ArrayBuffer(16)) }, + expectedInstanceOf: DataView, + match: (cloned) => { + return cloned.buffer.byteLength === 16; + } + }, + { + name: 'Date', + source: { value: new Date('2024-02-19 14:13:25') }, + expectedInstanceOf: Date, + match: (cloned) => { + return cloned.valueOf() === 1708348405000; // milliseconds for since the epoch for date + } + }, + { + name: 'Error', + source: { value: new TypeError('foo') }, + expectedInstanceOf: Error, + match: (cloned) => { + return cloned.message === 'foo'; + } + }, + { + name: 'Map', + source: { value: new Map([ + [ 'a', 1 ], + [ 'b', 2 ], + [ 'c', 3 ], + ]) }, + expectedInstanceOf: Map, + match: (cloned) => { + return cloned.has('a') && cloned.get('a') === 1 + && cloned.has('b') && cloned.get('b') === 2 + && cloned.has('c') && cloned.get('c') === 3 + } + }, + { + name: 'Number', + source: { value: new Number(42) }, + expectedInstanceOf: Number, + match: (cloned) => { + return cloned.valueOf() === 42; + } + }, + { + name: 'RegEx', + source: { value: new RegExp("bar", "g") }, + expectedInstanceOf: RegExp, + match: (cloned) => { + return cloned.toString() === '/bar/g' + } + }, + { + name: 'Set', + source: { value: new Set([ 1, 2, 3 ]) }, + expectedInstanceOf: Set, + match: (cloned) => { + return cloned.has(1) + && cloned.has(2) + && cloned.has(3); + } + }, + { + name: 'String', + source: { value: new String('John Doe') }, + expectedInstanceOf: String, + match: (cloned) => { + return cloned.valueOf() === 'John Doe'; + } + }, + { + name: 'TypedArray', + source: { value: new Int16Array(new ArrayBuffer(16)) }, + expectedInstanceOf: TYPED_ARRAY_PROTOTYPE, + match: (cloned) => { + return cloned.byteLength === 16; + } + }, + ]; + + // --------------------------------------------------------------------- // + + for (const entry of dataSet) { + const target = {}; + + const result = merge([ target, entry.source ]); + + expect(Reflect.has(result, 'value')) + .withContext(`No value property in result for ${entry.name}`) + .toBeTrue(); + + expect(result.value instanceof entry.expectedInstanceOf) + .withContext(`Invalid instanceof for ${entry.name}`) + .toBeTrue(); + + expect(result.value === entry.source.value) + .withContext(`Shallow copy was made for ${entry.name}`) + .toBeFalse(); + + expect(entry.match(result.value)) + .withContext(`Custom value match failed for ${entry.name}`) + .toBeTrue(); + } + }); }); }); \ No newline at end of file From 2d63a3151500eb981a35223f6c0299349ee94afd Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 15:14:44 +0100 Subject: [PATCH 169/424] Fix context message --- tests/browser/packages/support/arrays/isTypedArray.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/packages/support/arrays/isTypedArray.test.js b/tests/browser/packages/support/arrays/isTypedArray.test.js index 8dc18bbc..562ae166 100644 --- a/tests/browser/packages/support/arrays/isTypedArray.test.js +++ b/tests/browser/packages/support/arrays/isTypedArray.test.js @@ -23,7 +23,7 @@ describe('@aedart/support/arrays', () => { for (const data of dataSet) { expect(isTypedArray(data.value)) - .withContext(`${name} was expected to ${data.expected.toString()}`) + .withContext(`${data.name} was expected to ${data.expected.toString()}`) .toBe(data.expected); } }); From 1e5acc000404112f265e327ec84a8f9ffc00569d Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 15:38:53 +0100 Subject: [PATCH 170/424] Add isConcatSpreadable() util function --- packages/support/src/arrays/index.ts | 1 + .../support/src/arrays/isConcatSpreadable.ts | 21 +++++ .../support/arrays/isConcatSpreadable.test.js | 82 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 packages/support/src/arrays/isConcatSpreadable.ts create mode 100644 tests/browser/packages/support/arrays/isConcatSpreadable.test.js diff --git a/packages/support/src/arrays/index.ts b/packages/support/src/arrays/index.ts index 8e1056ee..aeb0a4ff 100644 --- a/packages/support/src/arrays/index.ts +++ b/packages/support/src/arrays/index.ts @@ -1,3 +1,4 @@ +export * from './isConcatSpreadable'; export * from './isTypedArray'; export * from './merge'; diff --git a/packages/support/src/arrays/isConcatSpreadable.ts b/packages/support/src/arrays/isConcatSpreadable.ts new file mode 100644 index 00000000..df0e8a3e --- /dev/null +++ b/packages/support/src/arrays/isConcatSpreadable.ts @@ -0,0 +1,21 @@ +/** + * Determine if target object is "concat spreadable" + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable + * + * @param {object} target + * + * @return {boolean} + */ +export function isConcatSpreadable(target: object): boolean +{ + return typeof target == 'object' + + // Must have symbol and it must be set to true + && Reflect.has(target, Symbol.isConcatSpreadable) + && target[Symbol.isConcatSpreadable] === true + + // But, a `length` property MUST also be present and be greater than or equal to 0 + && Reflect.has(target, 'length') + && target.length >= 0; +} \ No newline at end of file diff --git a/tests/browser/packages/support/arrays/isConcatSpreadable.test.js b/tests/browser/packages/support/arrays/isConcatSpreadable.test.js new file mode 100644 index 00000000..e64e9462 --- /dev/null +++ b/tests/browser/packages/support/arrays/isConcatSpreadable.test.js @@ -0,0 +1,82 @@ +import { isConcatSpreadable } from "@aedart/support/arrays"; + +describe('@aedart/support/arrays', () => { + describe('isConcatSpreadable()', () => { + + it('can determine if object is concat spreadable', () => { + + const concatSpreadableArr = [ 1, 2, 3 ]; + concatSpreadableArr[Symbol.isConcatSpreadable] = true; + + class A { + get [Symbol.isConcatSpreadable]() { + return true; + } + + get length() { + return 2 + } + + 0 = 'yes'; + 1 = 'no'; + } + + const dataSet = [ + // Now this is funny... an array does not inherit Symbol.isConcatSpreadable ! + { value: [ 1, 2, 3 ], expected: false, name: 'Array' }, + + { value: concatSpreadableArr, expected: true, name: 'Array with Symbol.isConcatSpreadable' }, + { + value: { + [Symbol.isConcatSpreadable]: true, + //length: 3, // NOTE: length is required. + 0: 'a', + 1: 'b', + 2: 'c' + }, + expected: false, + name: 'Object with Symbol.isConcatSpreadable' + }, + { + value: { + [Symbol.isConcatSpreadable]: true, + length: 3, + 0: 'a', + 1: 'b', + 2: 'c' + }, + expected: true, + name: 'Object with Symbol.isConcatSpreadable and length property' + }, + { + value: new A(), + expected: true, + name: 'Class instance with Symbol.isConcatSpreadable and length property' + }, + ]; + + for (const data of dataSet) { + const result = isConcatSpreadable(data.value); + + // Debug + // console.log('result', data.name, result); + + expect(result) + .withContext(`${data.name} was expected to ${data.expected.toString()}`) + .toBe(data.expected); + + if (data.expected === true) { + const target = [ 'foo', 'bar' ]; + const concat = [].concat(target, data.value); + + // Debug + // console.log('concat', concat); + + expect(concat.length) + .withContext('Unable to concat') + .toBe(target.length + data.value.length); + } + } + }); + }); +}); \ No newline at end of file From c407b2f68d7897ad1e1be15efad7e61cb21fd9da Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 16:20:36 +0100 Subject: [PATCH 171/424] Fix isConcatSpreadable() Ups... misunderstood the meaning of the well-known Symbol. Util function and tests have now been adapted. --- .../support/src/arrays/isConcatSpreadable.ts | 12 +---- .../support/arrays/isConcatSpreadable.test.js | 54 +++++++------------ 2 files changed, 21 insertions(+), 45 deletions(-) diff --git a/packages/support/src/arrays/isConcatSpreadable.ts b/packages/support/src/arrays/isConcatSpreadable.ts index df0e8a3e..773ea4fa 100644 --- a/packages/support/src/arrays/isConcatSpreadable.ts +++ b/packages/support/src/arrays/isConcatSpreadable.ts @@ -1,5 +1,5 @@ /** - * Determine if target object is "concat spreadable" + * Determine if target object contains the well-known symbol {@link Symbol.isConcatSpreadable} * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable * @@ -9,13 +9,5 @@ */ export function isConcatSpreadable(target: object): boolean { - return typeof target == 'object' - - // Must have symbol and it must be set to true - && Reflect.has(target, Symbol.isConcatSpreadable) - && target[Symbol.isConcatSpreadable] === true - - // But, a `length` property MUST also be present and be greater than or equal to 0 - && Reflect.has(target, 'length') - && target.length >= 0; + return Reflect.has(target, Symbol.isConcatSpreadable); } \ No newline at end of file diff --git a/tests/browser/packages/support/arrays/isConcatSpreadable.test.js b/tests/browser/packages/support/arrays/isConcatSpreadable.test.js index e64e9462..25455c42 100644 --- a/tests/browser/packages/support/arrays/isConcatSpreadable.test.js +++ b/tests/browser/packages/support/arrays/isConcatSpreadable.test.js @@ -3,79 +3,63 @@ import { isConcatSpreadable } from "@aedart/support/arrays"; describe('@aedart/support/arrays', () => { describe('isConcatSpreadable()', () => { - it('can determine if object is concat spreadable', () => { - + it('can determine if object contains Symbol.isConcatSpreadable', () => { + const concatSpreadableArr = [ 1, 2, 3 ]; concatSpreadableArr[Symbol.isConcatSpreadable] = true; + + class A {} - class A { - get [Symbol.isConcatSpreadable]() { - return true; - } - - get length() { - return 2 - } - - 0 = 'yes'; - 1 = 'no'; + class B { + [Symbol.isConcatSpreadable] = false; } const dataSet = [ - // Now this is funny... an array does not inherit Symbol.isConcatSpreadable ! + // Now this is funny... an array does Symbol.isConcatSpreadable ! { value: [ 1, 2, 3 ], expected: false, name: 'Array' }, - + { value: concatSpreadableArr, expected: true, name: 'Array with Symbol.isConcatSpreadable' }, { value: { - [Symbol.isConcatSpreadable]: true, - //length: 3, // NOTE: length is required. 0: 'a', 1: 'b', 2: 'c' }, expected: false, - name: 'Object with Symbol.isConcatSpreadable' + name: 'Object without Symbol.isConcatSpreadable' }, { value: { [Symbol.isConcatSpreadable]: true, - length: 3, + //length: 3, // NOTE: length should be implemented when Symbol.isConcatSpreadable set to true! 0: 'a', 1: 'b', 2: 'c' }, expected: true, - name: 'Object with Symbol.isConcatSpreadable and length property' + name: 'Object with Symbol.isConcatSpreadable' }, { value: new A(), + expected: false, + name: 'Class instance without Symbol.isConcatSpreadable' + }, + { + value: new B(), expected: true, - name: 'Class instance with Symbol.isConcatSpreadable and length property' + name: 'Class instance with Symbol.isConcatSpreadable' }, ]; for (const data of dataSet) { - const result = isConcatSpreadable(data.value); - + const result = isConcatSpreadable(data.value); + // Debug // console.log('result', data.name, result); expect(result) .withContext(`${data.name} was expected to ${data.expected.toString()}`) .toBe(data.expected); - - if (data.expected === true) { - const target = [ 'foo', 'bar' ]; - const concat = [].concat(target, data.value); - - // Debug - // console.log('concat', concat); - - expect(concat.length) - .withContext('Unable to concat') - .toBe(target.length + data.value.length); - } } }); }); From 6baf7bc14f9ef3fbd8a6a5d680824f347f7449da Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 16:25:23 +0100 Subject: [PATCH 172/424] Add ConcatSpreaddable interface --- .../src/support/arrays/ConcatSpreadable.ts | 30 +++++++++++++++++++ .../contracts/src/support/arrays/index.ts | 7 ++++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 packages/contracts/src/support/arrays/ConcatSpreadable.ts diff --git a/packages/contracts/src/support/arrays/ConcatSpreadable.ts b/packages/contracts/src/support/arrays/ConcatSpreadable.ts new file mode 100644 index 00000000..feee2dd4 --- /dev/null +++ b/packages/contracts/src/support/arrays/ConcatSpreadable.ts @@ -0,0 +1,30 @@ +/** + * Concat Spreadable + * + * Controls the behaviour of to treat this object's properties, when the object is concatenated via [`Array.concat()`]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat} + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable + */ +export default interface ConcatSpreadable extends ArrayLike +{ + /** + * Determines how [`Array.concat()`]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat} + * should treat this object as an array-like object and flattened to its array elements, or not. + * + * For array objects, the default behavior is to spread (flatten) elements (`[Symbol.isConcatSpreadable] === true`). + * + * For array-like objects, the default behavior is no spreading or flattening (`[Symbol.isConcatSpreadable] === false`). + * + * @type {boolean} + */ + [Symbol.isConcatSpreadable]: boolean; + + /** + * Length controls the number of properties to be added + * when this object is concatenated via [`Array.concat()`]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat}, + * if [Symbol.isConcatSpreadable]{@link Symbol.isConcatSpreadable} is `true`. + * + * @type {number} Must be greater than or equal to 0 + */ + readonly length: number; +} \ No newline at end of file diff --git a/packages/contracts/src/support/arrays/index.ts b/packages/contracts/src/support/arrays/index.ts index 63b35b0c..09aac224 100644 --- a/packages/contracts/src/support/arrays/index.ts +++ b/packages/contracts/src/support/arrays/index.ts @@ -3,4 +3,9 @@ * * @type {Symbol} */ -export const SUPPORT_ARRAYS: unique symbol = Symbol('@aedart/contracts/support/arrays'); \ No newline at end of file +export const SUPPORT_ARRAYS: unique symbol = Symbol('@aedart/contracts/support/arrays'); + +import ConcatSpreadable from "./ConcatSpreadable"; +export { + type ConcatSpreadable +} \ No newline at end of file From fe6dc1b9ef46fe437025cb78d534d4b42375bda7 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 16:25:30 +0100 Subject: [PATCH 173/424] Change release notes --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6537b41b..56d9b0c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `@aedart/contracts/support/exceptions` and `@aedart/support/exceptions` submodules. * `@aedart/contracts/support/objects` submodule. * `@aedart/contracts/support/arrays` and `@aedart/support/arrays` submodules. +* `ConcatSpreadable` (_extends TypeScript's `ArrayLike` interface_) interface in `@aedart/contracts/support/arrays`. * `Throwable` (_extends TypeScript's `Error` interface_) interface in `@aedart/contracts/support/exceptions`. * `LogicalError` and `AbstractClassError` exceptions in `@aedart/support/exceptions`. * `getErrorMessage()` in `@aedart/support/exceptions`. @@ -23,8 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `isSubclass()` in `@aedart/support/reflections`. * `classOwnKeys()` in `@aedart/support/reflections`. * `merge()` in `@aedart/support/objects`. -* `merge()` in `@aedart/support/arrays`. -* `isTypedArray()` in `@aedart/support/arrays`. +* `merge()`, `isTypedArray()` and `isConcatSpreadable()` in `@aedart/support/arrays`. ## [0.8.0] - 2024-02-12 From d8e4e64dc1bc89457310a008104f29a0f9b22a7c Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 16:39:20 +0100 Subject: [PATCH 174/424] Add todo Still need to handle Array-like objects --- packages/support/src/objects/merge.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index 9f1db8ff..ce0d106e 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -153,6 +153,7 @@ export const defaultMergeCallback: MergeCallback = function( return mergeArrays(value); } + // TODO: What about Array-Like / ConcatSpreadable ??? // Objects (of native kind) - - - - - - - - - - - - - - - - - - - - - - - - - // Clone the object value, if possible. From eedccc4a894e187bc5e8a0f1bc42373be2ace880 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 19 Feb 2024 16:43:39 +0100 Subject: [PATCH 175/424] Add reference to Symbol.isConcatSpreadable --- packages/support/src/arrays/merge.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/support/src/arrays/merge.ts b/packages/support/src/arrays/merge.ts index cf308667..b847d919 100644 --- a/packages/support/src/arrays/merge.ts +++ b/packages/support/src/arrays/merge.ts @@ -5,6 +5,9 @@ import { ArrayMergeError } from "./exceptions"; * * **Note**: _Method attempts to deep copy array values, via [structuredClone]{@link https://developer.mozilla.org/en-US/docs/Web/API/structuredClone}_ * + * @see https://developer.mozilla.org/en-US/docs/Web/API/structuredClone + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable + * * @param {...any[]} sources * * @return {any[]} From bff5135cb516317b02568cd38928633a5c0869aa Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 20 Feb 2024 09:59:04 +0100 Subject: [PATCH 176/424] Add isArrayLike() util function --- packages/support/src/arrays/index.ts | 1 + packages/support/src/arrays/isArrayLike.ts | 14 +++++++ .../support/arrays/isArrayLike.test.js | 41 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 packages/support/src/arrays/isArrayLike.ts create mode 100644 tests/browser/packages/support/arrays/isArrayLike.test.js diff --git a/packages/support/src/arrays/index.ts b/packages/support/src/arrays/index.ts index aeb0a4ff..322d04b2 100644 --- a/packages/support/src/arrays/index.ts +++ b/packages/support/src/arrays/index.ts @@ -1,3 +1,4 @@ +export * from './isArrayLike'; export * from './isConcatSpreadable'; export * from './isTypedArray'; export * from './merge'; diff --git a/packages/support/src/arrays/isArrayLike.ts b/packages/support/src/arrays/isArrayLike.ts new file mode 100644 index 00000000..5ec95296 --- /dev/null +++ b/packages/support/src/arrays/isArrayLike.ts @@ -0,0 +1,14 @@ +import { isArrayLike as _isArrayLike } from "lodash-es"; + +/** + * Determine if value is "array-like". + * (Alias for Lodash's [`isArrayLike`]{@link import('lodash').isArrayLike}) method. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#array-like_objects + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Indexed_collections#working_with_array-like_objects + * + * @param {any} value + * + * @return {boolean} + */ +export const isArrayLike = _isArrayLike; \ No newline at end of file diff --git a/tests/browser/packages/support/arrays/isArrayLike.test.js b/tests/browser/packages/support/arrays/isArrayLike.test.js new file mode 100644 index 00000000..fec57b07 --- /dev/null +++ b/tests/browser/packages/support/arrays/isArrayLike.test.js @@ -0,0 +1,41 @@ +import { isArrayLike } from "@aedart/support/arrays"; + +describe('@aedart/support/arrays', () => { + describe('isArrayLike()', () => { + + it('can determine if is array-like', () => { + + const dataSet = [ + { value: [], expected: true, name: 'Array' }, + { value: 'abc', expected: true, name: 'string' }, + { value: { length: 0 }, expected: true, name: 'Object (with length property)' }, + + // Well, String() object does have a `length` property + { value: new String('abc'), expected: true, name: 'String (object)' }, + + // TypeArrays have a length property too! + { value: new Int8Array(), expected: true, name: 'TypedArray' }, + + // -------------------------------------------------------------------------------- // + // These should never be considered array-like... + + { value: new Boolean(true), expected: false, name: 'Boolean' }, + { value: new Number(123), expected: false, name: 'Number' }, + { value: {}, expected: false, name: 'Object (without length property)' }, + { value: new Map(), expected: false, name: 'Map' }, + { value: new Set(), expected: false, name: 'Set' }, + { value: function() {}, expected: false, name: 'Function' }, + { value: new Date(), expected: false, name: 'Date' }, + { value: new ArrayBuffer(2), expected: false, name: 'ArrayBuffer' }, + { value: new DataView(new ArrayBuffer(2)), expected: false, name: 'DataView' }, + { value: new RegExp(/ab/g), expected: false, name: 'RegExp' }, + ]; + + for (const data of dataSet) { + expect(isArrayLike(data.value)) + .withContext(`${data.name} was expected to ${data.expected.toString()}`) + .toBe(data.expected); + } + }); + }); +}); \ No newline at end of file From 217eb013057d65d59ca63d4b1946c3cba97f524c Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 20 Feb 2024 10:04:09 +0100 Subject: [PATCH 177/424] Change release notes --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56d9b0c2..20787aee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `isSubclass()` in `@aedart/support/reflections`. * `classOwnKeys()` in `@aedart/support/reflections`. * `merge()` in `@aedart/support/objects`. -* `merge()`, `isTypedArray()` and `isConcatSpreadable()` in `@aedart/support/arrays`. +* `merge()`, `isTypedArray()`, `isArrayLike` and `isConcatSpreadable()` in `@aedart/support/arrays`. ## [0.8.0] - 2024-02-12 From f0aa220b665eea84bbdc2b5671b286be99813aba Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 20 Feb 2024 11:06:36 +0100 Subject: [PATCH 178/424] Add handling of concat spreadable objects --- packages/support/src/objects/merge.ts | 26 +++++++--- .../packages/support/objects/merge.test.js | 48 +++++++++++++++++++ 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index ce0d106e..490ac5cf 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -9,7 +9,10 @@ import { } from "@aedart/contracts/support/objects"; import type { Constructor } from "@aedart/contracts"; import { descTag } from "@aedart/support/misc"; -import { merge as mergeArrays } from "@aedart/support/arrays"; +import { + isConcatSpreadable, + merge as mergeArrays +} from "@aedart/support/arrays"; import { getErrorMessage } from "@aedart/support/exceptions"; import { TYPED_ARRAY_PROTOTYPE } from "@aedart/contracts/support/reflections"; import MergeError from "./exceptions/MergeError"; @@ -139,24 +142,33 @@ export const defaultMergeCallback: MergeCallback = function( } // Arrays - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if (Array.isArray(value)) { + const isArray: boolean = Array.isArray(value); + const concatSpreadable: boolean = isConcatSpreadable(value); + if (isArray || concatSpreadable) { // Use cloned array values, to avoid unintended manipulation of the original array values (if contains objects). // However, if the array contains non-cloneable values, then this can fail. // Merge array values if required. if (options.mergeArrays === true && Reflect.has(result, key) - && Array.isArray(result[key]) + && (Array.isArray(result[key]) || isConcatSpreadable(result[key])) ) { return mergeArrays(result[key], value); } - return mergeArrays(value); + // When the value is not concat spreadable, then overwrite it with new array or array-like + // value. Regular (basic object) merge will handle concat spreadable object. + if (!concatSpreadable) { + return mergeArrays(value); + } } - // TODO: What about Array-Like / ConcatSpreadable ??? + // TODO: What about Array-Like vs. String().length vs. TypedArrays,...etc + + // Objects (when cloneable) - - - - - - - - - - - - - - - - - - - - - - - - - + // TODO: See what others do... perhaps an interface / Symbol - // Objects (of native kind) - - - - - - - - - - - - - - - - - - - - - - - - - - // Clone the object value, if possible. + // Objects (of "native" kind) - - - - - - - - - - - - - - - - - - - - - - - - + // Clone the object of a "native" kind value, if supported. if (canCloneObjectValue(value)) { return structuredClone(value); } diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js index 5702c7f6..10a44f94 100644 --- a/tests/browser/packages/support/objects/merge.test.js +++ b/tests/browser/packages/support/objects/merge.test.js @@ -292,6 +292,54 @@ describe('@aedart/support/objects', () => { .withContext('Array value not merged') .toBe(expected); }); + + it('can merge concat spreadable object values', () => { + + const a = { + 'a': [ 1, 2, 3 ], + 'b': [ 'foo' ], + 'c': { + [Symbol.isConcatSpreadable]: true, + length: 1, + 0: 'bar', + }, + }; + const b = { + 'a': { + [Symbol.isConcatSpreadable]: true, + length: 3, + 0: 'a', + 1: 'b', + 2: 'c' + }, + 'b': { + [Symbol.isConcatSpreadable]: false, + length: 2, + 0: 'bar', + 1: 'zar', + }, + 'c': [ 'foo' ] + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ], { mergeArrays: true }); + + // Debug + console.log('result', result); + + expect(result.a) + .withContext('a) Array was not merged correctly with concat spreadable set to true') + .toEqual([ 1, 2, 3, 'a', 'b', 'c' ]); + + expect(JSON.stringify(result.b)) + .withContext('b) Array was not merged correctly with concat spreadable set to false') + .toBe(JSON.stringify([ 'foo', b['b'] ])); + + expect(result.c) + .withContext('c) Merged failed on top of object with concat spreadable set to true') + .toEqual([ 'bar', 'foo' ]); + }); it('fails when array values contain none-cloneable values', () => { From 774bae0bb3825cea6c86037ed797767b6acc7f1c Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 20 Feb 2024 11:08:56 +0100 Subject: [PATCH 179/424] Move concat spreadable test further down --- .../packages/support/objects/merge.test.js | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js index 10a44f94..70cf5d2a 100644 --- a/tests/browser/packages/support/objects/merge.test.js +++ b/tests/browser/packages/support/objects/merge.test.js @@ -293,6 +293,25 @@ describe('@aedart/support/objects', () => { .toBe(expected); }); + it('fails when array values contain none-cloneable values', () => { + + const callback = () => { + const a = { + 'arr': [ 1, 2, 3 ] + }; + const b = { + 'arr': [ function() {} ] + }; + + return merge([ a, b ] ); + } + + // --------------------------------------------------------------------- // + + expect(callback) + .toThrowError(MergeError); + }); + it('can merge concat spreadable object values', () => { const a = { @@ -341,25 +360,6 @@ describe('@aedart/support/objects', () => { .toEqual([ 'bar', 'foo' ]); }); - it('fails when array values contain none-cloneable values', () => { - - const callback = () => { - const a = { - 'arr': [ 1, 2, 3 ] - }; - const b = { - 'arr': [ function() {} ] - }; - - return merge([ a, b ] ); - } - - // --------------------------------------------------------------------- // - - expect(callback) - .toThrowError(MergeError); - }); - it('creates shallow copy of functions', () => { const a = { 'foo': null, From 5990e1e19d08c36d13c5c97f167cf68b45473473 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 20 Feb 2024 13:30:45 +0100 Subject: [PATCH 180/424] Add handling of array-like objects Took a bit of tinkering to find a good enough way to decide when to convert values into an array. However, it a nutshell this should be fine for most use-cases. Anything beyond that, the developer must provide own merge callback. --- packages/support/src/objects/merge.ts | 48 +++++-- .../packages/support/objects/merge.test.js | 122 +++++++++++++++++- 2 files changed, 156 insertions(+), 14 deletions(-) diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index 490ac5cf..fa64ddf7 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -10,7 +10,9 @@ import { import type { Constructor } from "@aedart/contracts"; import { descTag } from "@aedart/support/misc"; import { + isArrayLike, isConcatSpreadable, + isTypedArray, merge as mergeArrays } from "@aedart/support/arrays"; import { getErrorMessage } from "@aedart/support/exceptions"; @@ -143,26 +145,26 @@ export const defaultMergeCallback: MergeCallback = function( // Arrays - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const isArray: boolean = Array.isArray(value); - const concatSpreadable: boolean = isConcatSpreadable(value); - if (isArray || concatSpreadable) { - // Use cloned array values, to avoid unintended manipulation of the original array values (if contains objects). - // However, if the array contains non-cloneable values, then this can fail. - // Merge array values if required. + if (isArray || isConcatSpreadable(value) || isSafeArrayLike(value)) { + + // If required to merge with existing value, if one exists... if (options.mergeArrays === true && Reflect.has(result, key) - && (Array.isArray(result[key]) || isConcatSpreadable(result[key])) + && (isArray || Array.isArray(result[key])) ) { + // If either existing or new value is of the type array, merge values into + // a new array. return mergeArrays(result[key], value); - } - - // When the value is not concat spreadable, then overwrite it with new array or array-like - // value. Regular (basic object) merge will handle concat spreadable object. - if (!concatSpreadable) { + } else if (isArray) { + // When not requested merged, just overwrite existing value with a new array, + // if new value is an array. return mergeArrays(value); } + + // For concat spreadable objects or array-like objects, the "basic object" merge logic + // will deal with them. } - // TODO: What about Array-Like vs. String().length vs. TypedArrays,...etc // Objects (when cloneable) - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: See what others do... perhaps an interface / Symbol @@ -179,7 +181,11 @@ export const defaultMergeCallback: MergeCallback = function( // Objects (basic)- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Merge with existing, if existing value is not null... - if (Reflect.has(result, key) && typeof result[key] == 'object' && result[key] !== null) { + if (Reflect.has(result, key) + && typeof result[key] == 'object' + && result[key] !== null + && !(Array.isArray(result[key])) + ) { return performMerge([ result[key], value ], options, depth + 1); } @@ -300,6 +306,22 @@ function canCloneObjectValue(value: object): boolean return false; } +/** + * Determine if value is array-like, but not a String object or Typed Array + * + * @internal + * + * @param {object} value + * + * @return {boolean} + */ +function isSafeArrayLike(value: object): boolean +{ + return isArrayLike(value) + && !(value instanceof String) // String object handled by structured clone + && !isTypedArray(value); // TypedArray object handled by structured clone +} + /** * Resolve the merge options * diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js index 70cf5d2a..31a42618 100644 --- a/tests/browser/packages/support/objects/merge.test.js +++ b/tests/browser/packages/support/objects/merge.test.js @@ -345,7 +345,7 @@ describe('@aedart/support/objects', () => { const result = merge([ a, b ], { mergeArrays: true }); // Debug - console.log('result', result); + // console.log('result', result); expect(result.a) .withContext('a) Array was not merged correctly with concat spreadable set to true') @@ -359,6 +359,126 @@ describe('@aedart/support/objects', () => { .withContext('c) Merged failed on top of object with concat spreadable set to true') .toEqual([ 'bar', 'foo' ]); }); + + it('does not merge array-like objects by default', () => { + + const a = { + 'a': [ 1, 2, 3 ], + 'b': { + length: 2, + 0: 'a', + 1: 'b', + }, + 'c': { + length: 1, + 0: 'foo', + }, + 'd': [ 3, 4, 5 ], + 'e': [ 6, 7, 8 ], + }; + const b = { + 'a': { + length: 2, + 0: 'a', + 1: 'b', + }, + 'b': [ 'foo' ], + 'c': { + length: 2, + 1: 'bar', + }, + 'd': new String('foo'), + 'e': new Int8Array(2) + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ]); + + // Debug + // console.log('result', result); + + expect(JSON.stringify(result.a)) + .withContext('a) failed to overwrite existing value with array-like object') + .toBe(JSON.stringify({ 0: 'a', 1: 'b', length: 2 })); + + expect(JSON.stringify(result.b)) + .withContext('b) failed to overwrite existing array-like value with array') + .toBe(JSON.stringify([ 'foo' ])); + + expect(JSON.stringify(result.c)) + .withContext('c) failed to merge existing array-like value with array-like object') + .toBe(JSON.stringify({ 0: 'foo', 1: 'bar', length: 2 })); + + expect(result.d) + .withContext('d) String object should not be considered array-like (in this context)') + .toBeInstanceOf(String); + + expect(result.e) + .withContext('e) Typed Array object should not be considered array-like (in this context)') + .toBeInstanceOf(Int8Array); + }); + + it('can merge array-like objects', () => { + + const a = { + 'a': [ 1, 2, 3 ], + 'b': { + // NOTE: Object is not concat spreadable, so entire object should be expected! + length: 2, + 0: 'a', + 1: 'b', + }, + 'c': { + length: 1, + 0: 'foo', + }, + 'd': [ 3, 4, 5 ], + 'e': [ 6, 7, 8 ], + }; + const b = { + 'a': { + // NOTE: Object is not concat spreadable, so entire object should be expected! + length: 2, + 0: 'a', + 1: 'b', + }, + 'b': [ 'foo' ], + 'c': { + length: 2, + 1: 'bar', + }, + 'd': new String('foo'), + 'e': new Int8Array(2) + }; + + // --------------------------------------------------------------------- // + + const result = merge([a, b], { mergeArrays: true }); + + // Debug + console.log('result', result); + + expect(JSON.stringify(result.a)) + .withContext('a) should have merged existing array with array-like object') + .toBe(JSON.stringify([1, 2, 3, {0: 'a', 1: 'b', length: 2}])); + + expect(JSON.stringify(result.b)) + .withContext('b) should have merged array-like object with an array') + .toBe(JSON.stringify([{0: 'a', 1: 'b', length: 2}, 'foo'])); + + expect(JSON.stringify(result.c)) + .withContext('c) failed to merge array-like value with array-like object') + .toBe(JSON.stringify({0: 'foo', 1: 'bar', length: 2})); + + expect(result.d) + .withContext('d) String object should not be considered array-like (in this context)') + .toBeInstanceOf(String); + + expect(result.e) + .withContext('e) Typed Array object should not be considered array-like (in this context)') + .toBeInstanceOf(Int8Array); + }); it('creates shallow copy of functions', () => { const a = { From 5895d3975e3afe2b8f5c0276458fd2bdcf778172 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 20 Feb 2024 13:32:05 +0100 Subject: [PATCH 181/424] Improve description of mergeArrays property --- .../contracts/src/support/objects/MergeOptions.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/contracts/src/support/objects/MergeOptions.ts b/packages/contracts/src/support/objects/MergeOptions.ts index 823f28c5..ff939cab 100644 --- a/packages/contracts/src/support/objects/MergeOptions.ts +++ b/packages/contracts/src/support/objects/MergeOptions.ts @@ -66,22 +66,24 @@ export default interface MergeOptions overwriteWithUndefined?: boolean; /** - * Flag, merge array properties + * Flag, whether to merge array, array-like, and [concat spreadable]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable} + * properties or not. * - * **When `true`**: _existing array property value is attempted merged with new array value._ + * **When `true`**: _existing property is merged with new property value._ * - * **When `false` (_default behaviour_)**: _existing array property value is overwritten with new array value_ + * **When `false` (_default behaviour_)**: _existing property is overwritten with new property value_ * * **Example:** * ```js - * const a { 'foo': [ 1, 2, 3 ] }; - * const a { 'foo': [ 4, 5, 6 ] }; + * const a = { 'foo': [ 1, 2, 3 ] }; + * const b = { 'foo': [ 4, 5, 6 ] }; * * merge([ a, b ]); // { 'foo': [ 4, 5, 6 ] } - * * merge([ a, b ], { mergeArrays: true }); // { 'foo': [ 1, 2, 3, 4, 5, 6 ] } * ``` * + * @see [merge (array)]{@link import('@aedart/support/arrays').merge} + * * @type {boolean} */ mergeArrays?: boolean, From a051032b96ac0f48f73ff670c85ef353d7016f0e Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 20 Feb 2024 13:59:13 +0100 Subject: [PATCH 182/424] Add note about String() and Typed Arrays, for mergeArrays property --- packages/contracts/src/support/objects/MergeOptions.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/contracts/src/support/objects/MergeOptions.ts b/packages/contracts/src/support/objects/MergeOptions.ts index ff939cab..617d0c9c 100644 --- a/packages/contracts/src/support/objects/MergeOptions.ts +++ b/packages/contracts/src/support/objects/MergeOptions.ts @@ -82,6 +82,10 @@ export default interface MergeOptions * merge([ a, b ], { mergeArrays: true }); // { 'foo': [ 1, 2, 3, 4, 5, 6 ] } * ``` * + * **Note**: _`String()` (object) and [Typed Arrays]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray} + * are not merged, even though they are considered to be "array-like" (they offer a `length` property). + * You need to manually handle these, via a custom [callback]{@link MergeCallback}, if such value types must be merged._ + * * @see [merge (array)]{@link import('@aedart/support/arrays').merge} * * @type {boolean} From 6abc224f6ef31ae5269b42cab6ebb46d854c4d0c Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 20 Feb 2024 14:07:38 +0100 Subject: [PATCH 183/424] Shorten the resolveOptions() --- packages/support/src/objects/merge.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index fa64ddf7..a35d803a 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -335,26 +335,26 @@ function isSafeArrayLike(value: object): boolean */ function resolveOptions(options?: MergeCallback | MergeOptions): Readonly { - // Resolve merge callback - const callback: MergeCallback = (typeof options == 'function') - ? options - : defaultMergeCallback; - - // Resolve user provided merge options - const userOptions: MergeOptions = (typeof options == 'object' && options !== null) - ? options - : Object.create(null); - // Merge the default and user provided options... const resolved: MergeOptions = { + // Use default options as a base. ...DEFAULT_MERGE_OPTIONS, + + // Resolve merge callback. ...{ - callback: callback + callback: (typeof options == 'function') + ? options + : defaultMergeCallback }, - ...userOptions + + // Resolve user provided merge options. + ...(typeof options == 'object' && options !== null) + ? options + : Object.create(null) }; - // Abort in case of invalid maximum depth + // Abort in case of invalid maximum depth - other options can also be asserted, but they are less important. + // The Browser / Node.js Engine will throw an error in case that they maximum recursion level is reached! if (typeof resolved.depth != 'number' || resolved.depth < 0) { throw new MergeError('Invalid maximum "depth" merge option value', { cause: { From 7bd46f6f4d1bac415d98003f384ba9b3da69a60a Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 20 Feb 2024 14:27:36 +0100 Subject: [PATCH 184/424] Add handling of Weak Reference kind objects --- packages/support/src/objects/merge.ts | 24 ++++++++++++-- .../packages/support/objects/merge.test.js | 33 +++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index a35d803a..c3ab7e93 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -175,9 +175,11 @@ export const defaultMergeCallback: MergeCallback = function( return structuredClone(value); } - // TODO: WeakMap ??? - // TODO: WeakSet ??? - // TODO: WeakRef ??? + // Objects (WeakRef, WeakMap and WeakSet) - - - - - - - - - - - - - - - - - - + // "Weak Reference" kind of objects cannot, nor should they, be cloned. + if (isWeakReferenceKind(value)) { + return value; + } // Objects (basic)- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Merge with existing, if existing value is not null... @@ -322,6 +324,22 @@ function isSafeArrayLike(value: object): boolean && !isTypedArray(value); // TypedArray object handled by structured clone } +/** + * Determine if object of a "weak reference" kind, e.g. `WeakRef`, `WeakMap` or `WeakSet` + * + * @internal + * + * @param {object} value + * + * @return {boolean} + */ +function isWeakReferenceKind(value: object): boolean +{ + return value instanceof WeakRef + || value instanceof WeakMap + || value instanceof WeakSet +} + /** * Resolve the merge options * diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js index 31a42618..01cba78d 100644 --- a/tests/browser/packages/support/objects/merge.test.js +++ b/tests/browser/packages/support/objects/merge.test.js @@ -738,5 +738,38 @@ describe('@aedart/support/objects', () => { .toBeTrue(); } }); + + it('does not clone objects of "Weak Reference" kind', () => { + + class A {} + + const a = {}; + const b = { + 'a' : new WeakRef(new A()), + 'b' : new WeakMap([ + [ new A(), 'foo' ] + ]), + 'c' : new WeakSet([ new A() ]) + } + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ]); + + // Debug + // console.log('result', result); + + expect(result['a'] === b['a']) + .withContext('a) WeakRef not same instance') + .toBeTrue(); + + expect(result['b'] === b['b']) + .withContext('b) WeakMap not same instance') + .toBeTrue(); + + expect(result['c'] === b['c']) + .withContext('c) WeakSet not same instance') + .toBeTrue(); + }); }); }); \ No newline at end of file From 25bd7a4f1ffc3337c2c0951e648fac6e2034aff9 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 20 Feb 2024 15:06:10 +0100 Subject: [PATCH 185/424] Add Cloneable interface --- .../contracts/src/support/objects/Cloneable.ts | 14 ++++++++++++++ packages/contracts/src/support/objects/index.ts | 4 +++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 packages/contracts/src/support/objects/Cloneable.ts diff --git a/packages/contracts/src/support/objects/Cloneable.ts b/packages/contracts/src/support/objects/Cloneable.ts new file mode 100644 index 00000000..5203cc54 --- /dev/null +++ b/packages/contracts/src/support/objects/Cloneable.ts @@ -0,0 +1,14 @@ +/** + * Cloneable + */ +export default interface Cloneable +{ + /** + * Returns a clone (new instance) of this object + * + * @return {this} + * + * @throws {Error} + */ + clone(): this; +} \ No newline at end of file diff --git a/packages/contracts/src/support/objects/index.ts b/packages/contracts/src/support/objects/index.ts index 5f606a9c..f8383afd 100644 --- a/packages/contracts/src/support/objects/index.ts +++ b/packages/contracts/src/support/objects/index.ts @@ -19,9 +19,11 @@ export const DEFAULT_MERGE_SKIP_KEYS: PropertyKey[] = [ 'prototype', '__proto__' */ export const DEFAULT_MAX_MERGE_DEPTH: number = 512; +import Cloneable from "./Cloneable"; import MergeOptions from "./MergeOptions"; export { - type MergeOptions + type Cloneable, + type MergeOptions, } export * from './types'; \ No newline at end of file From bf958b99e16d6e2913827960942dfdbb9da48a60 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 20 Feb 2024 15:33:38 +0100 Subject: [PATCH 186/424] Fix null causes isConcatSpreadable to fail --- packages/support/src/arrays/isConcatSpreadable.ts | 4 +++- .../packages/support/arrays/isConcatSpreadable.test.js | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/support/src/arrays/isConcatSpreadable.ts b/packages/support/src/arrays/isConcatSpreadable.ts index 773ea4fa..4e6824cd 100644 --- a/packages/support/src/arrays/isConcatSpreadable.ts +++ b/packages/support/src/arrays/isConcatSpreadable.ts @@ -9,5 +9,7 @@ */ export function isConcatSpreadable(target: object): boolean { - return Reflect.has(target, Symbol.isConcatSpreadable); + return typeof target == 'object' + && target !== null + && Reflect.has(target, Symbol.isConcatSpreadable); } \ No newline at end of file diff --git a/tests/browser/packages/support/arrays/isConcatSpreadable.test.js b/tests/browser/packages/support/arrays/isConcatSpreadable.test.js index 25455c42..2656b843 100644 --- a/tests/browser/packages/support/arrays/isConcatSpreadable.test.js +++ b/tests/browser/packages/support/arrays/isConcatSpreadable.test.js @@ -15,6 +15,8 @@ describe('@aedart/support/arrays', () => { } const dataSet = [ + { value: null, expected: false, name: 'Null' }, + // Now this is funny... an array does Symbol.isConcatSpreadable ! { value: [ 1, 2, 3 ], expected: false, name: 'Array' }, From 5b38bf6931bf9d1b4265013d6201b78858aa7ced Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 20 Feb 2024 15:34:34 +0100 Subject: [PATCH 187/424] Add isCloneable() util function --- packages/support/src/objects/index.ts | 1 + packages/support/src/objects/isCloneable.ts | 17 +++++++++++++ .../support/objects/isCloneable.test.js | 25 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 packages/support/src/objects/isCloneable.ts create mode 100644 tests/browser/packages/support/objects/isCloneable.test.js diff --git a/packages/support/src/objects/index.ts b/packages/support/src/objects/index.ts index a810f42f..6b8d6e23 100644 --- a/packages/support/src/objects/index.ts +++ b/packages/support/src/objects/index.ts @@ -5,6 +5,7 @@ export * from './has'; export * from './hasAll'; export * from './hasAny'; export * from './hasUniqueId'; +export * from './isCloneable'; export * from './isset'; export * from './merge'; export * from './set'; diff --git a/packages/support/src/objects/isCloneable.ts b/packages/support/src/objects/isCloneable.ts new file mode 100644 index 00000000..aa03329a --- /dev/null +++ b/packages/support/src/objects/isCloneable.ts @@ -0,0 +1,17 @@ +/** + * Determine if target object is cloneable. + * + * **Note**: _Method assumes that target is cloneable if it implements the + * [Cloneable]{@link import('@aedart/constracts/support/objects').Cloneable} interface._ + * + * @param {object} target + * + * @return {boolean} + */ +export function isCloneable(target: object): boolean +{ + return typeof target == 'object' + && target !== null + && Reflect.has(target, 'clone') + && typeof target.clone == 'function'; +} \ No newline at end of file diff --git a/tests/browser/packages/support/objects/isCloneable.test.js b/tests/browser/packages/support/objects/isCloneable.test.js new file mode 100644 index 00000000..e215d0cd --- /dev/null +++ b/tests/browser/packages/support/objects/isCloneable.test.js @@ -0,0 +1,25 @@ +import { isCloneable } from "@aedart/support/objects"; + +describe('@aedart/support/objects', () => { + describe('isCloneable', () => { + + it('can determine if is cloneable', () => { + + const dataSet = [ + { value: [], expected: false, name: 'Array' }, + { value: null, expected: false, name: 'Null' }, + { value: {}, expected: false, name: 'Object' }, + { value: { clone: false }, expected: false, name: 'Object with clone property' }, + + { value: { clone: () => true }, expected: true, name: 'Object with clone function' }, + ]; + + for (const data of dataSet) { + expect(isCloneable(data.value)) + .withContext(`${data.name} was expected to ${data.expected.toString()}`) + .toBe(data.expected); + } + }); + + }); +}); \ No newline at end of file From 4b0f705decd99cb32e9681a76fd420fa578dbb37 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 20 Feb 2024 16:09:22 +0100 Subject: [PATCH 188/424] Add support for Cloneable objects Will now invoke clone() method and use resulting object to merge. --- packages/support/src/objects/merge.ts | 51 +++++++++++++------ .../packages/support/objects/merge.test.js | 36 +++++++++++++ 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index c3ab7e93..ca8b79d3 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -1,7 +1,8 @@ import type { MergeOptions, MergeCallback, - SkipKeyCallback + SkipKeyCallback, + Cloneable } from "@aedart/contracts/support/objects"; import { DEFAULT_MAX_MERGE_DEPTH, @@ -18,6 +19,7 @@ import { import { getErrorMessage } from "@aedart/support/exceptions"; import { TYPED_ARRAY_PROTOTYPE } from "@aedart/contracts/support/reflections"; import MergeError from "./exceptions/MergeError"; +import { isCloneable } from "@aedart/support/objects/isCloneable"; /** * Default merge options to be applied, when none are provided to {@link merge} @@ -33,7 +35,10 @@ export const DEFAULT_MERGE_OPTIONS: MergeOptions = { Object.freeze(DEFAULT_MERGE_OPTIONS); /** - * Merge two or more objects + * Deep merge two or more objects + * + * **Note**: _If a source object implements the {@link Cloneable} interface, then the return value of its clone() method + * is iterated and merged into target object. The cloned object is resolved before the {@link MergeCallback} is applied._ * * @param {object[]} sources * @param {MergeCallback|MergeOptions} [options] Merge callback or merge options. If merge options are given, @@ -75,7 +80,6 @@ export function merge( } } - /** * Default merge callback * @@ -102,7 +106,7 @@ export const defaultMergeCallback: MergeCallback = function( ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { // Determine the type and resolve value based on it... - const type: string = typeof value; + const type: string = typeof value; switch (type) { @@ -144,7 +148,7 @@ export const defaultMergeCallback: MergeCallback = function( } // Arrays - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const isArray: boolean = Array.isArray(value); + const isArray: boolean = Array.isArray(value); /* eslint-disable-line no-case-declarations */ if (isArray || isConcatSpreadable(value) || isSafeArrayLike(value)) { @@ -165,9 +169,6 @@ export const defaultMergeCallback: MergeCallback = function( // For concat spreadable objects or array-like objects, the "basic object" merge logic // will deal with them. } - - // Objects (when cloneable) - - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: See what others do... perhaps an interface / Symbol // Objects (of "native" kind) - - - - - - - - - - - - - - - - - - - - - - - - // Clone the object of a "native" kind value, if supported. @@ -236,20 +237,40 @@ function performMerge(sources: object[], options: Readonly, depth: } return sources.reduce((result: object, source: object, index: number) => { - if (Array.isArray(source)) { - throw new MergeError(`Unable to merge object with an array source, (source index: ${index})`, { + // Abort if source is invalid... + if (!source || typeof source != 'object' || Array.isArray(source)) { + throw new MergeError(`Unable to merge source of invalid type "${descTag(source)}" (source index: ${index})`, { cause: { source: source, index: index, depth: depth } - }); + }); } - const keys: PropertyKey[] = Reflect.ownKeys(source); + // Favour "clone()" method return object instead of the source object, if the source implements + // the Cloneable interface. + const cloneable: boolean = isCloneable(source); + let resolvedSource: object = cloneable + ? (source as Cloneable).clone() + : source; + + // Abort if clone() returned invalid type... + if (cloneable && (!resolvedSource || typeof resolvedSource != 'object' || Array.isArray(resolvedSource))) { + throw new MergeError(`Expected clone() method to return object for source, (source index: ${index})`, { + cause: { + source: source, + index: index, + depth: depth + } + }); + } + + // Iterate through all properties, including symbols + const keys: PropertyKey[] = Reflect.ownKeys(resolvedSource); for (const key of keys){ // Skip key if needed ... - if ((options.skip as SkipKeyCallback)(key, source, result)) { + if ((options.skip as SkipKeyCallback)(key, resolvedSource, result)) { continue; } @@ -257,8 +278,8 @@ function performMerge(sources: object[], options: Readonly, depth: result[key] = options.callback( result, key, - source[key], - source, + resolvedSource[key], + resolvedSource, index, depth, options diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js index 01cba78d..8ea1317f 100644 --- a/tests/browser/packages/support/objects/merge.test.js +++ b/tests/browser/packages/support/objects/merge.test.js @@ -771,5 +771,41 @@ describe('@aedart/support/objects', () => { .withContext('c) WeakSet not same instance') .toBeTrue(); }); + + it('favours cloneable object\'s clone() method', () => { + + const a = { + a: { + name: 'John', + age: 42 + } + }; + + const b = { + a: { + name: 'John', // Property should be ignored, due to clone() + + clone: () => { + return { + name: 'Rick' + } + } + } + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ]); + + // Debug + // console.log('result', result); + + expect(result.a.name) + .withContext('Clone method not favoured') + .toBe('Rick'); + expect(result.a.age) + .withContext('Other properties are not merged in correctly') + .toBe(42) + }); }); }); \ No newline at end of file From 4d448005beaa4c89275a4e527f45a68e5fe1d5ba Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 20 Feb 2024 16:09:34 +0100 Subject: [PATCH 189/424] Change release notes --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20787aee..9ffe9ffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `@aedart/contracts/support/arrays` and `@aedart/support/arrays` submodules. * `ConcatSpreadable` (_extends TypeScript's `ArrayLike` interface_) interface in `@aedart/contracts/support/arrays`. * `Throwable` (_extends TypeScript's `Error` interface_) interface in `@aedart/contracts/support/exceptions`. +* `Cloneable` interface in `@aedart/contracts/support/objects`. * `LogicalError` and `AbstractClassError` exceptions in `@aedart/support/exceptions`. * `getErrorMessage()` in `@aedart/support/exceptions`. * `FUNCTION_PROTOTYPE` and `TYPED_ARRAY_PROTOTYPE` constants in `@aedart/contracts/support/reflections`. @@ -25,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `classOwnKeys()` in `@aedart/support/reflections`. * `merge()` in `@aedart/support/objects`. * `merge()`, `isTypedArray()`, `isArrayLike` and `isConcatSpreadable()` in `@aedart/support/arrays`. +* `isCloneable()` in `@aedart/support/objects`. ## [0.8.0] - 2024-02-12 From 62c3b63ab42636a5dc3fa264b91ec8f9eb86fa93 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 20 Feb 2024 20:59:17 +0100 Subject: [PATCH 190/424] Fix types IDE / TypeScript complained about these being nullable, which is true - but in practice these should never result in null values. --- packages/contracts/src/support/reflections/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/reflections/index.ts b/packages/contracts/src/support/reflections/index.ts index b7c2dad3..7d9f122f 100644 --- a/packages/contracts/src/support/reflections/index.ts +++ b/packages/contracts/src/support/reflections/index.ts @@ -14,7 +14,7 @@ export const SUPPORT_REFLECTIONS: unique symbol = Symbol('@aedart/contracts/supp * * @type {object} */ -export const FUNCTION_PROTOTYPE: object = Reflect.getPrototypeOf(Function); +export const FUNCTION_PROTOTYPE: object = Reflect.getPrototypeOf(Function) as object; /** * `TypedArray` prototype @@ -25,4 +25,4 @@ export const FUNCTION_PROTOTYPE: object = Reflect.getPrototypeOf(Function); * * @type {object} */ -export const TYPED_ARRAY_PROTOTYPE: object = Reflect.getPrototypeOf(Int8Array); +export const TYPED_ARRAY_PROTOTYPE: object = Reflect.getPrototypeOf(Int8Array) as object; From ba774ba8fa9b0144c0a3eb797136feee8708ceb4 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 20 Feb 2024 21:18:11 +0100 Subject: [PATCH 191/424] Add "useCloneable" option Refactored to allow developers to disable the cloneable behavior. --- .../src/support/objects/MergeOptions.ts | 32 ++++++++++ packages/support/src/objects/merge.ts | 58 +++++++++++------- .../packages/support/objects/merge.test.js | 60 ++++++++++++++++++- 3 files changed, 128 insertions(+), 22 deletions(-) diff --git a/packages/contracts/src/support/objects/MergeOptions.ts b/packages/contracts/src/support/objects/MergeOptions.ts index 617d0c9c..e155b69a 100644 --- a/packages/contracts/src/support/objects/MergeOptions.ts +++ b/packages/contracts/src/support/objects/MergeOptions.ts @@ -65,6 +65,38 @@ export default interface MergeOptions */ overwriteWithUndefined?: boolean; + /** + * Flag, if source object is [`Cloneable`]{@link import('@aedart/contracts/support/objects').Cloneable}, then the + * resulting object from the `clone()` method is used. + * + * **When `true` (_default behaviour_)**: _If source object is cloneable then the resulting object from `clone()` + * method is used. Its properties are then iterated by the merge function._ + * + * **When `false`**: _Cloneable objects are treated like any other objects, the `clone()` method is ignored._ + * + * **Example:** + * ```js + * const a = { 'foo': { 'name': 'John Doe' } }; + * const b = { 'foo': { + * 'name': 'Jane Doe', + * clone() { + * return { + * 'name': 'Rick Doe', + * 'age': 26 + * } + * } + * } }; + * + * merge([ a, b ]); // { 'foo': { 'name': 'Rick Doe', 'age': 26 } } + * merge([ a, b ], { useCloneable: false }); // { 'foo': { 'name': 'Jane Doe', clone() {...} } } + * ``` + * + * @see [`Cloneable`]{@link import('@aedart/contracts/support/objects').Cloneable} + * + * @type {boolean} + */ + useCloneable?: boolean; + /** * Flag, whether to merge array, array-like, and [concat spreadable]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable} * properties or not. diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index ca8b79d3..e7cfd74e 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -30,6 +30,7 @@ export const DEFAULT_MERGE_OPTIONS: MergeOptions = { depth: DEFAULT_MAX_MERGE_DEPTH, skip: DEFAULT_MERGE_SKIP_KEYS, overwriteWithUndefined: true, + useCloneable: true, mergeArrays: false, }; Object.freeze(DEFAULT_MERGE_OPTIONS); @@ -172,7 +173,7 @@ export const defaultMergeCallback: MergeCallback = function( // Objects (of "native" kind) - - - - - - - - - - - - - - - - - - - - - - - - // Clone the object of a "native" kind value, if supported. - if (canCloneObjectValue(value)) { + if (canCloneUsingStructuredClone(value)) { return structuredClone(value); } @@ -227,7 +228,7 @@ export const defaultMergeCallback: MergeCallback = function( function performMerge(sources: object[], options: Readonly, depth: number = 0): object { // Abort if maximum depth has been reached - if (depth > options.depth) { + if (depth > (options.depth as number)) { throw new MergeError(`Maximum merge depth (${options.depth}) has been exceeded`, { cause: { source: sources, @@ -248,22 +249,11 @@ function performMerge(sources: object[], options: Readonly, depth: }); } - // Favour "clone()" method return object instead of the source object, if the source implements - // the Cloneable interface. - const cloneable: boolean = isCloneable(source); - let resolvedSource: object = cloneable - ? (source as Cloneable).clone() - : source; - - // Abort if clone() returned invalid type... - if (cloneable && (!resolvedSource || typeof resolvedSource != 'object' || Array.isArray(resolvedSource))) { - throw new MergeError(`Expected clone() method to return object for source, (source index: ${index})`, { - cause: { - source: source, - index: index, - depth: depth - } - }); + let resolvedSource: object = source; + + // If allowed and source implements "Cloneable" interface, favour "clone()" method's resulting object. + if (options.useCloneable && isCloneable(source)) { + resolvedSource = cloneSource(source as Cloneable); } // Iterate through all properties, including symbols @@ -275,7 +265,7 @@ function performMerge(sources: object[], options: Readonly, depth: } // Resolve the value via callback and set it in resulting object. - result[key] = options.callback( + result[key] = (options.callback as MergeCallback)( result, key, resolvedSource[key], @@ -290,6 +280,32 @@ function performMerge(sources: object[], options: Readonly, depth: }, Object.create(null)); } +/** + * Returns source object's clone, from it's + * + * @internal + * + * @param {Cloneable} source + * + * @returns {object} + */ +function cloneSource(source: Cloneable): object +{ + const clone: object = source.clone(); + + // Abort if resulting value from "clone()" is not a valid value... + if (!clone || typeof clone != 'object' || Array.isArray(clone)) { + throw new MergeError(`Expected clone() method to return object for source, ${descTag(clone)} was returned`, { + cause: { + source: source, + clone: clone, + } + }); + } + + return clone; +} + /** * Determine if an object value can be cloned via `structuredClone()` * @@ -301,7 +317,7 @@ function performMerge(sources: object[], options: Readonly, depth: * * @return {boolean} */ -function canCloneObjectValue(value: object): boolean +function canCloneUsingStructuredClone(value: object): boolean { const supported: Constructor[] = [ // Array, // Handled by array, with evt. array value merges @@ -317,7 +333,7 @@ function canCloneObjectValue(value: object): boolean RegExp, Set, String, - TYPED_ARRAY_PROTOTYPE + TYPED_ARRAY_PROTOTYPE as Constructor ]; for (const constructor of supported) { diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js index 8ea1317f..1ebbe1b4 100644 --- a/tests/browser/packages/support/objects/merge.test.js +++ b/tests/browser/packages/support/objects/merge.test.js @@ -457,7 +457,7 @@ describe('@aedart/support/objects', () => { const result = merge([a, b], { mergeArrays: true }); // Debug - console.log('result', result); + // console.log('result', result); expect(JSON.stringify(result.a)) .withContext('a) should have merged existing array with array-like object') @@ -807,5 +807,63 @@ describe('@aedart/support/objects', () => { .withContext('Other properties are not merged in correctly') .toBe(42) }); + + it('can disable cloneable behaviour', () => { + + const a = { + a: { + name: 'John', + } + }; + + const b = { + a: { + name: 'Jim', + clone: () => { + return { + name: 'Rick' + } + } + } + }; + + // --------------------------------------------------------------------- // + + const result = merge([ a, b ], { useCloneable: false }); + + // Debug + // console.log('result', result); + + expect(result.a.name) + .withContext('Clone was not disabled') + .toBe('Jim'); + }); + + it('fails when cloneable source returns invalid value', () => { + + const a = { + a: { + name: 'John', + } + }; + + const b = { + a: { + name: 'Jim', + clone: () => { + return null; // Should cause error + } + } + }; + + // --------------------------------------------------------------------- // + + const callback = () => { + return merge([ a, b ]); + } + + expect(callback) + .toThrowError(MergeError); + }); }); }); \ No newline at end of file From 6b6c5fc3bd3288e589e1665ab9791750979c3fdd Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 21 Feb 2024 09:08:55 +0100 Subject: [PATCH 192/424] Fix missing semicolon --- packages/contracts/src/support/objects/MergeOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/objects/MergeOptions.ts b/packages/contracts/src/support/objects/MergeOptions.ts index e155b69a..c5f866b8 100644 --- a/packages/contracts/src/support/objects/MergeOptions.ts +++ b/packages/contracts/src/support/objects/MergeOptions.ts @@ -122,7 +122,7 @@ export default interface MergeOptions * * @type {boolean} */ - mergeArrays?: boolean, + mergeArrays?: boolean; /** * The merge callback that must be applied From 20ef048f9f4dc8e4792efe954a3076489670a102 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 21 Feb 2024 15:25:52 +0100 Subject: [PATCH 193/424] Move Merge Options to sub-dir --- .../src/support/objects/{ => merge}/MergeOptions.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) rename packages/contracts/src/support/objects/{ => merge}/MergeOptions.ts (98%) diff --git a/packages/contracts/src/support/objects/MergeOptions.ts b/packages/contracts/src/support/objects/merge/MergeOptions.ts similarity index 98% rename from packages/contracts/src/support/objects/MergeOptions.ts rename to packages/contracts/src/support/objects/merge/MergeOptions.ts index c5f866b8..245b15df 100644 --- a/packages/contracts/src/support/objects/MergeOptions.ts +++ b/packages/contracts/src/support/objects/merge/MergeOptions.ts @@ -1,4 +1,7 @@ -import type { MergeCallback, SkipKeyCallback } from "./types"; +import type { + MergeCallback, + SkipKeyCallback +} from "./types"; /** * Merge Options From d7a058bcb3991bd6e9787c4e22fd98f2fad34cd2 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 21 Feb 2024 15:26:32 +0100 Subject: [PATCH 194/424] Extract isSafeArrayLike() into array submodule --- packages/support/src/arrays/index.ts | 1 + .../support/src/arrays/isSafeArrayLike.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 packages/support/src/arrays/isSafeArrayLike.ts diff --git a/packages/support/src/arrays/index.ts b/packages/support/src/arrays/index.ts index 322d04b2..99de386d 100644 --- a/packages/support/src/arrays/index.ts +++ b/packages/support/src/arrays/index.ts @@ -1,5 +1,6 @@ export * from './isArrayLike'; export * from './isConcatSpreadable'; +export * from './isSafeArrayLike'; export * from './isTypedArray'; export * from './merge'; diff --git a/packages/support/src/arrays/isSafeArrayLike.ts b/packages/support/src/arrays/isSafeArrayLike.ts new file mode 100644 index 00000000..3449cad3 --- /dev/null +++ b/packages/support/src/arrays/isSafeArrayLike.ts @@ -0,0 +1,19 @@ +import { isArrayLike } from './isArrayLike'; +import { isTypedArray } from './isTypedArray'; + +/** + * Determine if value is "safe" array-like object + * + * **Note**: _In this context "safe" means that given object passes {@link isArrayLike}, + * but is not instance of a {@link String} object, nor is the object a [Typed Array]{@link isTypedArray}!_ + * + * @param {object} value + * + * @return {boolean} + */ +export function isSafeArrayLike(value: object): boolean +{ + return isArrayLike(value) + && !(value instanceof String) + && !isTypedArray(value); +} \ No newline at end of file From 658d785463e4ab1b8203e816116c70800385c0b7 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 21 Feb 2024 15:26:40 +0100 Subject: [PATCH 195/424] Fix type --- packages/support/src/arrays/isTypedArray.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/support/src/arrays/isTypedArray.ts b/packages/support/src/arrays/isTypedArray.ts index 2047983f..19a721f8 100644 --- a/packages/support/src/arrays/isTypedArray.ts +++ b/packages/support/src/arrays/isTypedArray.ts @@ -1,4 +1,5 @@ import { TYPED_ARRAY_PROTOTYPE } from "@aedart/contracts/support/reflections"; +import { Constructor } from "@aedart/contracts"; /** * Determine if given target is an instance of a `TypedArray` @@ -11,5 +12,5 @@ import { TYPED_ARRAY_PROTOTYPE } from "@aedart/contracts/support/reflections"; */ export function isTypedArray(target: object): boolean { - return target instanceof TYPED_ARRAY_PROTOTYPE; + return target instanceof (TYPED_ARRAY_PROTOTYPE as Constructor); } \ No newline at end of file From ead89cc632396b15dd9fea58a804a41418b347ca Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 21 Feb 2024 15:27:30 +0100 Subject: [PATCH 196/424] Extract is "weak" kind into reflections submodule Previous version was named isWeakrefereneKind() --- packages/support/src/reflections/index.ts | 3 ++- packages/support/src/reflections/isWeakKind.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 packages/support/src/reflections/isWeakKind.ts diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index 77ff7e62..9f6f204d 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -10,4 +10,5 @@ export * from './hasPrototypeProperty'; export * from './isCallable'; export * from './isClassConstructor'; export * from './isConstructor'; -export * from './isSubclass'; \ No newline at end of file +export * from './isSubclass'; +export * from './isWeakKind'; \ No newline at end of file diff --git a/packages/support/src/reflections/isWeakKind.ts b/packages/support/src/reflections/isWeakKind.ts new file mode 100644 index 00000000..8c3e645f --- /dev/null +++ b/packages/support/src/reflections/isWeakKind.ts @@ -0,0 +1,15 @@ +/** + * Determine if object of a "weak" kind, e.g. `WeakRef`, `WeakMap` or `WeakSet` + * + * @param {object} value + * + * @return {boolean} + */ +export function isWeakKind(value: object): boolean +{ + return value && ( + value instanceof WeakRef + || value instanceof WeakMap + || value instanceof WeakSet + ); +} \ No newline at end of file From 4d5878f1c2405cbaabcd96a546e0dca94c948826 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 21 Feb 2024 16:06:16 +0100 Subject: [PATCH 197/424] Refactor / Redesign merge() to use an Objects Merger instance This is so much cleaner to read - even though a bit complex. Module functions should be small, and not contain too many exports, so this way developers can freely extend the new Objects Merger and create their own merge util method(s). --- .../contracts/src/support/objects/index.ts | 17 +- .../support/objects/merge/MergeException.ts | 8 + .../src/support/objects/merge/MergeOptions.ts | 2 +- .../support/objects/merge/MergeSourceInfo.ts | 48 ++ .../support/objects/merge/ObjectsMerger.ts | 160 +++++++ .../src/support/objects/merge/index.ts | 24 + .../contracts/src/support/objects/types.ts | 66 ++- .../src/objects/exceptions/MergeError.ts | 6 +- packages/support/src/objects/index.ts | 4 +- packages/support/src/objects/merge.ts | 446 +----------------- .../src/objects/merge/DefaultMergeOptions.ts | 214 +++++++++ packages/support/src/objects/merge/Merger.ts | 285 +++++++++++ .../merge/canCloneUsingStructuredClone.ts | 41 ++ packages/support/src/objects/merge/index.ts | 10 + .../objects/merge/makeDefaultMergeCallback.ts | 137 ++++++ .../objects/merge/makeDefaultSkipCallback.ts | 19 + packages/support/src/objects/merger.ts | 18 + .../packages/support/objects/merge.test.js | 7 +- 18 files changed, 1042 insertions(+), 470 deletions(-) create mode 100644 packages/contracts/src/support/objects/merge/MergeException.ts create mode 100644 packages/contracts/src/support/objects/merge/MergeSourceInfo.ts create mode 100644 packages/contracts/src/support/objects/merge/ObjectsMerger.ts create mode 100644 packages/contracts/src/support/objects/merge/index.ts create mode 100644 packages/support/src/objects/merge/DefaultMergeOptions.ts create mode 100644 packages/support/src/objects/merge/Merger.ts create mode 100644 packages/support/src/objects/merge/canCloneUsingStructuredClone.ts create mode 100644 packages/support/src/objects/merge/index.ts create mode 100644 packages/support/src/objects/merge/makeDefaultMergeCallback.ts create mode 100644 packages/support/src/objects/merge/makeDefaultSkipCallback.ts create mode 100644 packages/support/src/objects/merger.ts diff --git a/packages/contracts/src/support/objects/index.ts b/packages/contracts/src/support/objects/index.ts index f8383afd..a46f11e2 100644 --- a/packages/contracts/src/support/objects/index.ts +++ b/packages/contracts/src/support/objects/index.ts @@ -5,25 +5,10 @@ */ export const SUPPORT_OBJECTS: unique symbol = Symbol('@aedart/contracts/support/objects'); -/** - * Default property keys to be skipped when merging objects - * - * @type {PropertyKey[]} - */ -export const DEFAULT_MERGE_SKIP_KEYS: PropertyKey[] = [ 'prototype', '__proto__' ]; - -/** - * Default maximum merge depth - * - * @type {number} - */ -export const DEFAULT_MAX_MERGE_DEPTH: number = 512; - import Cloneable from "./Cloneable"; -import MergeOptions from "./MergeOptions"; export { type Cloneable, - type MergeOptions, } +export * from './merge'; export * from './types'; \ No newline at end of file diff --git a/packages/contracts/src/support/objects/merge/MergeException.ts b/packages/contracts/src/support/objects/merge/MergeException.ts new file mode 100644 index 00000000..8bf49a68 --- /dev/null +++ b/packages/contracts/src/support/objects/merge/MergeException.ts @@ -0,0 +1,8 @@ +import type { Throwable } from "@aedart/contracts/support/exceptions"; + +/** + * Merge Exception + * + * To be thrown when two or more objects are unable to be merged. + */ +export default interface MergeException extends Throwable {} \ No newline at end of file diff --git a/packages/contracts/src/support/objects/merge/MergeOptions.ts b/packages/contracts/src/support/objects/merge/MergeOptions.ts index 245b15df..c71df5b3 100644 --- a/packages/contracts/src/support/objects/merge/MergeOptions.ts +++ b/packages/contracts/src/support/objects/merge/MergeOptions.ts @@ -1,7 +1,7 @@ import type { MergeCallback, SkipKeyCallback -} from "./types"; +} from "../types"; /** * Merge Options diff --git a/packages/contracts/src/support/objects/merge/MergeSourceInfo.ts b/packages/contracts/src/support/objects/merge/MergeSourceInfo.ts new file mode 100644 index 00000000..51521bf8 --- /dev/null +++ b/packages/contracts/src/support/objects/merge/MergeSourceInfo.ts @@ -0,0 +1,48 @@ +/** + * Merge Source Info + * + * Contains information about what key-value must be merged from a target source object, + * along with other meta information. + */ +export default interface MergeSourceInfo +{ + /** + * The resulting object (relative to object depth) + * + * @type {object} + */ + result: object, + + /** + * The target property key in source object to + * + * @type {PropertyKey} + */ + key: PropertyKey, + + /** + * Value of the property in source object + * + * @type {any} + */ + value: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + /** + * The source object that holds the property key and value + * + * @type {object} + */ + source: object, + + /** + * Source object's index (relative to object depth) + * + * @type {number} + */ + sourceIndex: number, + + /** + * The current recursion depth + */ + depth: number, +} \ No newline at end of file diff --git a/packages/contracts/src/support/objects/merge/ObjectsMerger.ts b/packages/contracts/src/support/objects/merge/ObjectsMerger.ts new file mode 100644 index 00000000..dcd3a573 --- /dev/null +++ b/packages/contracts/src/support/objects/merge/ObjectsMerger.ts @@ -0,0 +1,160 @@ +import type {MergeCallback, MergeOptions} from "@aedart/contracts/support/objects"; + +/** + * Objects Merger + * + * Able to merge (deep merge) multiple source objects into a single new object. + */ +export default interface ObjectsMerger +{ + /** + * Use the following merge options or merge callback + * + * @param {MergeCallback | MergeOptions} [options] Merge callback or merge options. + * + * @return {this} + * + * @throws {MergeException} + */ + using(options?: MergeCallback | MergeOptions): this; + + /** + * Returns a merger of given source objects + * + * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} + * of all given sources._ + * + * @param {object} a + * + * @returns {object} + * + * @throws {MergeException} + */ + of< + SourceA extends object + >(a: SourceA): SourceA; + + /** + * Returns a merger of given source objects + * + * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} + * of all given sources._ + * + * @param {object} a + * @param {object} b + * + * @returns {object} + * + * @throws {MergeException} + */ + of< + SourceA extends object, + SourceB extends object, + >(a: SourceA, b: SourceB): SourceA & SourceB; + + /** + * Returns a merger of given source objects + * + * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} + * of all given sources._ + * + * @param {object} a + * @param {object} b + * @param {object} c + * + * @returns {object} + * + * @throws {MergeException} + */ + of< + SourceA extends object, + SourceB extends object, + SourceC extends object, + >(a: SourceA, b: SourceB, c: SourceC): SourceA & SourceB & SourceC; + + /** + * Returns a merger of given source objects + * + * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} + * of all given sources._ + * + * @param {object} a + * @param {object} b + * @param {object} c + * @param {object} d + * + * @returns {object} + * + * @throws {MergeException} + */ + of< + SourceA extends object, + SourceB extends object, + SourceC extends object, + SourceD extends object, + >(a: SourceA, b: SourceB, c: SourceC, d: SourceD): SourceA & SourceB & SourceC & SourceD; + + /** + * Returns a merger of given source objects + * + * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} + * of all given sources._ + * + * @param {object} a + * @param {object} b + * @param {object} c + * @param {object} d + * @param {object} e + * + * @returns {object} + * + * @throws {MergeException} + */ + of< + SourceA extends object, + SourceB extends object, + SourceC extends object, + SourceD extends object, + SourceE extends object, + >(a: SourceA, b: SourceB, c: SourceC, d: SourceD, e: SourceE): SourceA & SourceB & SourceC & SourceD & SourceE; + + /** + * Returns a merger of given source objects + * + * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} + * of all given sources._ + * + * @param {object} a + * @param {object} b + * @param {object} c + * @param {object} d + * @param {object} e + * @param {object} f + * + * @returns {object} + * + * @throws {MergeException} + */ + of< + SourceA extends object, + SourceB extends object, + SourceC extends object, + SourceD extends object, + SourceE extends object, + SourceF extends object, + >(a: SourceA, b: SourceB, c: SourceC, d: SourceD, e: SourceE, f: SourceF): SourceA & SourceB & SourceC & SourceD & SourceE & SourceF; + + /** + * Returns a merger of given source objects + * + * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} + * of all given sources._ + * + * @param {object[]} sources + * + * @returns {object} + * + * @throws {MergeException} + */ + of(...sources: object[]): object; +} \ No newline at end of file diff --git a/packages/contracts/src/support/objects/merge/index.ts b/packages/contracts/src/support/objects/merge/index.ts new file mode 100644 index 00000000..42dbc0e5 --- /dev/null +++ b/packages/contracts/src/support/objects/merge/index.ts @@ -0,0 +1,24 @@ +/** + * Default property keys to be skipped when merging objects + * + * @type {PropertyKey[]} + */ +export const DEFAULT_MERGE_SKIP_KEYS: PropertyKey[] = [ 'prototype', '__proto__' ]; + +/** + * Default maximum merge depth + * + * @type {number} + */ +export const DEFAULT_MAX_MERGE_DEPTH: number = 512; + +import MergeException from "./MergeException"; +import MergeSourceInfo from "./MergeSourceInfo"; +import MergeOptions from "./MergeOptions"; +import ObjectsMerger from "./ObjectsMerger"; +export { + type MergeException, + type MergeOptions, + type MergeSourceInfo, + type ObjectsMerger +} diff --git a/packages/contracts/src/support/objects/types.ts b/packages/contracts/src/support/objects/types.ts index b21e90d9..67845d4e 100644 --- a/packages/contracts/src/support/objects/types.ts +++ b/packages/contracts/src/support/objects/types.ts @@ -1,22 +1,68 @@ -import MergeOptions from "./MergeOptions"; +import type { + MergeOptions, + MergeSourceInfo +} from "./merge"; /** - * Merge callback function + * Callback to perform the merging of nested objects. + * Invoking this callback results in the merge callback to + * be repeated, for the given source objects. * + * @type {function} + */ +export type NextCallback = ( + + /** + * The nested objects to be merged + * + * @type {object[]} + */ + sources: object[], + + /** + * The merge options to be applied + * + * @type {Readonly} + */ + options: Readonly, + + /** + * The next recursion depth number + * + * @type {number} + */ + nextDepth: number +) => any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + +/** + * Merge callback function + * * The callback is responsible for resolving the value to be merged into the resulting object. - * + * * **Note**: _[Skipped keys]{@link MergeOptions.skip} are NOT provided to the callback._ - * + * * **Note**: _The callback is responsible for respecting the given [options]{@link MergeOptions}, * ([keys to be skipped]{@link MergeOptions.skip} and [depth]{@link MergeOptions.depth} excluded)_ */ export type MergeCallback = ( - result: object, - key: PropertyKey, - value: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ - source: object, - sourceIndex: number, - depth: number, + + /** + * Source target information + * + * @type {MergeSourceInfo} + */ + target: MergeSourceInfo, + + /** + * Callback to invoke for merging nested objects + * + * @type {function} + */ + next: NextCallback, + + /** + * The merge options to be applied + */ options: Readonly ) => any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ diff --git a/packages/support/src/objects/exceptions/MergeError.ts b/packages/support/src/objects/exceptions/MergeError.ts index 84d7b44b..dbdb702a 100644 --- a/packages/support/src/objects/exceptions/MergeError.ts +++ b/packages/support/src/objects/exceptions/MergeError.ts @@ -1,11 +1,11 @@ -import type { Throwable } from "@aedart/contracts/support/exceptions"; +import type { MergeException } from "@aedart/contracts/support/objects"; /** * Merge Error * - * To be thrown when two or more objects are unable to be merged. + * @see MergeException */ -export default class MergeError extends Error implements Throwable +export default class MergeError extends Error implements MergeException { /** * Create a new Merge Error instance diff --git a/packages/support/src/objects/index.ts b/packages/support/src/objects/index.ts index 6b8d6e23..5de37eef 100644 --- a/packages/support/src/objects/index.ts +++ b/packages/support/src/objects/index.ts @@ -8,6 +8,7 @@ export * from './hasUniqueId'; export * from './isCloneable'; export * from './isset'; export * from './merge'; +export * from './merger'; export * from './set'; export * from './uniqueId'; @@ -16,4 +17,5 @@ export { ObjectId } -export * from './exceptions'; \ No newline at end of file +export * from './exceptions'; +export * from './merge/index'; \ No newline at end of file diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index e7cfd74e..799e28fb 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -1,449 +1,21 @@ import type { MergeOptions, MergeCallback, - SkipKeyCallback, - Cloneable } from "@aedart/contracts/support/objects"; -import { - DEFAULT_MAX_MERGE_DEPTH, - DEFAULT_MERGE_SKIP_KEYS -} from "@aedart/contracts/support/objects"; -import type { Constructor } from "@aedart/contracts"; -import { descTag } from "@aedart/support/misc"; -import { - isArrayLike, - isConcatSpreadable, - isTypedArray, - merge as mergeArrays -} from "@aedart/support/arrays"; -import { getErrorMessage } from "@aedart/support/exceptions"; -import { TYPED_ARRAY_PROTOTYPE } from "@aedart/contracts/support/reflections"; -import MergeError from "./exceptions/MergeError"; -import { isCloneable } from "@aedart/support/objects/isCloneable"; - -/** - * Default merge options to be applied, when none are provided to {@link merge} - * - * @type {MergeOptions} - */ -export const DEFAULT_MERGE_OPTIONS: MergeOptions = { - depth: DEFAULT_MAX_MERGE_DEPTH, - skip: DEFAULT_MERGE_SKIP_KEYS, - overwriteWithUndefined: true, - useCloneable: true, - mergeArrays: false, -}; -Object.freeze(DEFAULT_MERGE_OPTIONS); - -/** - * Deep merge two or more objects - * - * **Note**: _If a source object implements the {@link Cloneable} interface, then the return value of its clone() method - * is iterated and merged into target object. The cloned object is resolved before the {@link MergeCallback} is applied._ - * - * @param {object[]} sources - * @param {MergeCallback|MergeOptions} [options] Merge callback or merge options. If merge options are given, - * then the `callback` setting is automatically set to {@link defaultMergeCallback} - * if not otherwise specified. - * - * @returns {object} - * - * @throws {MergeError} If unable to merge objects - */ -export function merge( - sources: object[], - options?: MergeCallback | MergeOptions -): object -{ - // Resolve the merge options - const resolved: Readonly = resolveOptions(options); - - // Perform actual merge... - try { - return performMerge(sources, resolved); - } catch (error) { - if (error instanceof MergeError) { - error.cause.sources = sources; - error.cause.options = resolved; - - throw error; - } - - const reason: string = getErrorMessage(error); - - throw new MergeError(`Unable to merge objects: ${reason}`, { - cause: { - previous: error, - sources: sources, - options: resolved - } - }); - } -} - -/** - * Default merge callback - * - * @param {object} result The resulting object (relative to object depth) - * @param {PropertyKey} key Property Key in source object - * @param {any} value Value of the property in source object - * @param {object} source The source object that holds the property - * @param {number} sourceIndex Source index (relative to object depth) - * @param {number} depth Current depth - * @param {Readonly} options - * - * @returns {any} The value to be merged into the resulting object - * - * @throws {MergeError} If unable to resolve value - */ -export const defaultMergeCallback: MergeCallback = function( - result: object, - key: PropertyKey, - value: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ - source: object, - sourceIndex: number, - depth: number, - options: Readonly -): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ -{ - // Determine the type and resolve value based on it... - const type: string = typeof value; - - switch (type) { - - // -------------------------------------------------------------------------------------------------------- // - // Primitives - // @see https://developer.mozilla.org/en-US/docs/Glossary/Primitive - - case 'undefined': - // Do not overwrite existing value with `undefined`, if options do not allow it... - if (value === undefined - && options.overwriteWithUndefined === false - && Reflect.has(result, key) - && result[key] !== undefined - ) { - return result[key]; - } - - return value; - - case 'string': - case 'number': - case 'bigint': - case 'boolean': - case 'symbol': - return value; - - // -------------------------------------------------------------------------------------------------------- // - // Functions - - case 'function': - return value; - - // -------------------------------------------------------------------------------------------------------- // - // Null, Arrays and Objects - case 'object': - // Null (primitive) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if (value === null) { - return value; - } - - // Arrays - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const isArray: boolean = Array.isArray(value); /* eslint-disable-line no-case-declarations */ - - if (isArray || isConcatSpreadable(value) || isSafeArrayLike(value)) { - - // If required to merge with existing value, if one exists... - if (options.mergeArrays === true - && Reflect.has(result, key) - && (isArray || Array.isArray(result[key])) - ) { - // If either existing or new value is of the type array, merge values into - // a new array. - return mergeArrays(result[key], value); - } else if (isArray) { - // When not requested merged, just overwrite existing value with a new array, - // if new value is an array. - return mergeArrays(value); - } - - // For concat spreadable objects or array-like objects, the "basic object" merge logic - // will deal with them. - } - - // Objects (of "native" kind) - - - - - - - - - - - - - - - - - - - - - - - - - // Clone the object of a "native" kind value, if supported. - if (canCloneUsingStructuredClone(value)) { - return structuredClone(value); - } - - // Objects (WeakRef, WeakMap and WeakSet) - - - - - - - - - - - - - - - - - - - // "Weak Reference" kind of objects cannot, nor should they, be cloned. - if (isWeakReferenceKind(value)) { - return value; - } - - // Objects (basic)- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Merge with existing, if existing value is not null... - if (Reflect.has(result, key) - && typeof result[key] == 'object' - && result[key] !== null - && !(Array.isArray(result[key])) - ) { - return performMerge([ result[key], value ], options, depth + 1); - } - - // Otherwise, create a new object and merge it. - return performMerge([ Object.create(null), value ], options, depth + 1); - - // -------------------------------------------------------------------------------------------------------- // - // If for some reason this point is reached, it means that we are unable to merge "something". - default: - throw new MergeError(`Unable to merge value of type ${type} (${descTag(value)}) at source index ${sourceIndex}`, { - cause: { - key: key, - value: value, - source: source, - sourceIndex: sourceIndex, - depth: depth, - options: options - } - }); - } -} +import { merger } from './merger' /** - * Performs merge of given objects - * - * @internal + * Returns a merger of given source objects * + * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} + * of all given sources._ + * * @param {object[]} sources - * @param {Readonly} options - * @param {number} [depth=0] - * - * @returns {object} - * - * @throws {MergeError} If unable to merge objects - */ -function performMerge(sources: object[], options: Readonly, depth: number = 0): object -{ - // Abort if maximum depth has been reached - if (depth > (options.depth as number)) { - throw new MergeError(`Maximum merge depth (${options.depth}) has been exceeded`, { - cause: { - source: sources, - depth: depth - } - }); - } - - return sources.reduce((result: object, source: object, index: number) => { - // Abort if source is invalid... - if (!source || typeof source != 'object' || Array.isArray(source)) { - throw new MergeError(`Unable to merge source of invalid type "${descTag(source)}" (source index: ${index})`, { - cause: { - source: source, - index: index, - depth: depth - } - }); - } - - let resolvedSource: object = source; - - // If allowed and source implements "Cloneable" interface, favour "clone()" method's resulting object. - if (options.useCloneable && isCloneable(source)) { - resolvedSource = cloneSource(source as Cloneable); - } - - // Iterate through all properties, including symbols - const keys: PropertyKey[] = Reflect.ownKeys(resolvedSource); - for (const key of keys){ - // Skip key if needed ... - if ((options.skip as SkipKeyCallback)(key, resolvedSource, result)) { - continue; - } - - // Resolve the value via callback and set it in resulting object. - result[key] = (options.callback as MergeCallback)( - result, - key, - resolvedSource[key], - resolvedSource, - index, - depth, - options - ); - } - - return result; - }, Object.create(null)); -} - -/** - * Returns source object's clone, from it's + * @param {MergeCallback | MergeOptions} [options] Merge callback or merge options. * - * @internal - * - * @param {Cloneable} source - * - * @returns {object} - */ -function cloneSource(source: Cloneable): object -{ - const clone: object = source.clone(); - - // Abort if resulting value from "clone()" is not a valid value... - if (!clone || typeof clone != 'object' || Array.isArray(clone)) { - throw new MergeError(`Expected clone() method to return object for source, ${descTag(clone)} was returned`, { - cause: { - source: source, - clone: clone, - } - }); - } - - return clone; -} - -/** - * Determine if an object value can be cloned via `structuredClone()` - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/structuredClone - * - * @internal - * - * @param {object} value - * - * @return {boolean} - */ -function canCloneUsingStructuredClone(value: object): boolean -{ - const supported: Constructor[] = [ - // Array, // Handled by array, with evt. array value merges - ArrayBuffer, - Boolean, - DataView, - Date, - Error, - Map, - Number, - // Object, // Handled by "basic" objects merging... - // (Primitive Types), // Also handled elsewhere... - RegExp, - Set, - String, - TYPED_ARRAY_PROTOTYPE as Constructor - ]; - - for (const constructor of supported) { - if (value instanceof constructor) { - return true; - } - } - - return false; -} - -/** - * Determine if value is array-like, but not a String object or Typed Array - * - * @internal - * - * @param {object} value - * - * @return {boolean} - */ -function isSafeArrayLike(value: object): boolean -{ - return isArrayLike(value) - && !(value instanceof String) // String object handled by structured clone - && !isTypedArray(value); // TypedArray object handled by structured clone -} - -/** - * Determine if object of a "weak reference" kind, e.g. `WeakRef`, `WeakMap` or `WeakSet` - * - * @internal - * - * @param {object} value - * - * @return {boolean} - */ -function isWeakReferenceKind(value: object): boolean -{ - return value instanceof WeakRef - || value instanceof WeakMap - || value instanceof WeakSet -} - -/** - * Resolve the merge options - * - * @internal - * - * @param {MergeCallback | MergeOptions} [options] - * - * @return {Readonly} - * * @throws {MergeError} */ -function resolveOptions(options?: MergeCallback | MergeOptions): Readonly -{ - // Merge the default and user provided options... - const resolved: MergeOptions = { - // Use default options as a base. - ...DEFAULT_MERGE_OPTIONS, - - // Resolve merge callback. - ...{ - callback: (typeof options == 'function') - ? options - : defaultMergeCallback - }, - - // Resolve user provided merge options. - ...(typeof options == 'object' && options !== null) - ? options - : Object.create(null) - }; - - // Abort in case of invalid maximum depth - other options can also be asserted, but they are less important. - // The Browser / Node.js Engine will throw an error in case that they maximum recursion level is reached! - if (typeof resolved.depth != 'number' || resolved.depth < 0) { - throw new MergeError('Invalid maximum "depth" merge option value', { - cause: { - options: resolved - } - }); - } - - // Resolve the skip callback. - if (Array.isArray(resolved.skip)) { - resolved.skip = makeDefaultSkipCallback(resolved.skip); - } - - // Freeze the resolved options to avoid strange behaviour, if user provides - // custom merge callback and attempts to change the options... - return Object.freeze(resolved); -} - -/** - * Returns a default "skip" callback, for given property keys - * - * @internal - * - * @param {PropertyKey[]} keys Properties that must not be merged - * - * @return {SkipKeyCallback} - */ -function makeDefaultSkipCallback(keys: PropertyKey[]): SkipKeyCallback +export function merge(sources: object[], options?: MergeCallback | MergeOptions) { - return ( - key: PropertyKey, - source: object, - result: object /* eslint-disable-line @typescript-eslint/no-unused-vars */ - ) => { - return keys.includes(key) && Reflect.has(source, key); - } -} + return merger(options).of(...sources); +} \ No newline at end of file diff --git a/packages/support/src/objects/merge/DefaultMergeOptions.ts b/packages/support/src/objects/merge/DefaultMergeOptions.ts new file mode 100644 index 00000000..49b9258c --- /dev/null +++ b/packages/support/src/objects/merge/DefaultMergeOptions.ts @@ -0,0 +1,214 @@ +import type { + MergeCallback, + MergeOptions, + SkipKeyCallback +} from "@aedart/contracts/support/objects"; +import { + DEFAULT_MAX_MERGE_DEPTH, + DEFAULT_MERGE_SKIP_KEYS +} from "@aedart/contracts/support/objects"; +import { MergeError } from "../exceptions"; +import { makeDefaultMergeCallback } from "./makeDefaultMergeCallback"; +import { makeDefaultSkipCallback } from "./makeDefaultSkipCallback"; + +/** + * Default Merge Options + * + * @see MergeOptions + */ +export default class DefaultMergeOptions implements MergeOptions +{ + /** + * The maximum merge depth + * + * **Note**: _Value must be greater than or equal zero._ + * + * **Note**: _Defaults to [DEFAULT_MAX_MERGE_DEPTH]{@link import('@aedart/contracts/support/objects').DEFAULT_MAX_MERGE_DEPTH} + * when not specified._ + * + * @type {number} + */ + depth: number = DEFAULT_MAX_MERGE_DEPTH; + + /** + * Property Keys that must not be merged. + * + * **Note**: _Defaults to [DEFAULT_MERGE_SKIP_KEYS]{@link import('@aedart/contracts/support/objects').DEFAULT_MERGE_SKIP_KEYS} + * when not specified._ + * + * **Callback**: _A callback can be specified to determine if a given key, + * in a source object should be skipped._ + * + * **Example:** + * ```js + * const a { 'foo': true }; + * const a { 'bar': true, 'zar': true }; + * + * merge([ a, b ], { skip: [ 'zar' ] }); // { 'foo': true, 'bar': true } + * + * merge([ a, b ], { skip: (key, source) => { + * return key === 'bar' && Reflect.has(source, key); + * } }); // { 'foo': true, 'zar': true } + * ``` + * + * @type {PropertyKey[] | SkipKeyCallback} + */ + skip: PropertyKey[] | SkipKeyCallback = DEFAULT_MERGE_SKIP_KEYS; + + /** + * Flag, overwrite property values with `undefined`. + * + * **When `true` (_default behaviour_)**: _If an existing property value is not `undefined`, it will be overwritten + * with new value, even if the new value is `undefined`._ + * + * **When `false`**: _If an existing property value is not `undefined`, it will NOT be overwritten + * with new value, if the new value is `undefined`._ + * + * **Example:** + * ```js + * const a { 'foo': true }; + * const a { 'foo': undefined }; + * + * merge([ a, b ]); // { 'foo': undefined } + * + * merge([ a, b ], { overwriteWithUndefined: false }); // { 'foo': true } + * ``` + * + * @type {boolean} + */ + overwriteWithUndefined: boolean = true; + + /** + * Flag, if source object is [`Cloneable`]{@link import('@aedart/contracts/support/objects').Cloneable}, then the + * resulting object from the `clone()` method is used. + * + * **When `true` (_default behaviour_)**: _If source object is cloneable then the resulting object from `clone()` + * method is used. Its properties are then iterated by the merge function._ + * + * **When `false`**: _Cloneable objects are treated like any other objects (`clone()` method ignored)._ + * + * **Example:** + * ```js + * const a = { 'foo': { 'name': 'John Doe' } }; + * const b = { 'foo': { + * 'name': 'Jane Doe', + * clone() { + * return { + * 'name': 'Rick Doe', + * 'age': 26 + * } + * } + * } }; + * + * merge([ a, b ]); // { 'foo': { 'name': 'Rick Doe', 'age': 26 } } + * merge([ a, b ], { useCloneable: false }); // { 'foo': { 'name': 'Jane Doe', clone() {...} } } + * ``` + * + * @see [`Cloneable`]{@link import('@aedart/contracts/support/objects').Cloneable} + * + * @type {boolean} + */ + useCloneable: boolean = true; + + /** + * Flag, whether to merge array, array-like, and [concat spreadable]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable} + * properties or not. + * + * **When `true`**: _existing property is merged with new property value._ + * + * **When `false` (_default behaviour_)**: _existing property is overwritten with new property value_ + * + * **Example:** + * ```js + * const a = { 'foo': [ 1, 2, 3 ] }; + * const b = { 'foo': [ 4, 5, 6 ] }; + * + * merge([ a, b ]); // { 'foo': [ 4, 5, 6 ] } + * merge([ a, b ], { mergeArrays: true }); // { 'foo': [ 1, 2, 3, 4, 5, 6 ] } + * ``` + * + * **Note**: _`String()` (object) and [Typed Arrays]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray} + * are not merged, even though they are considered to be "array-like" (they offer a `length` property). + * You need to manually handle these, via a custom [callback]{@link MergeCallback}, if such value types must be merged._ + * + * @see [merge (array)]{@link import('@aedart/support/arrays').merge} + * + * @type {boolean} + */ + mergeArrays: boolean = false; + + /** + * The merge callback that must be applied + * + * **Note**: _When no callback is provided, then the merge function's default + * callback is used._ + * + * @type {MergeCallback} + */ + callback: MergeCallback; + + /** + * Creates a new Merge Options instance + * + * @param {MergeCallback | MergeOptions} [options] + */ + public constructor(options?: MergeCallback | MergeOptions) { + // Merge provided options, if any given + if (options && typeof options == 'object') { + const supported: PropertyKey[] = Reflect.ownKeys(this) + .filter((key: PropertyKey) => { + return !['__proto__', 'prototype', 'constructor', 'makeDefaultSkipCallback'].includes(key as string); + }); + + Reflect.ownKeys(options) + .forEach((key) => { + if (!supported.includes(key)) { + const k = (typeof key == 'symbol') + ? key.description + : key; + + throw new MergeError(`Unsupported merge option key: ${k}`); + } + + // @ts-expect-error We are safe to set option value + this[key] = options[key]; + }); + } + + // Abort in case of invalid maximum depth - other options can also be asserted, but they are less important. + // The Browser / Node.js Engine will throw an error in case that they maximum recursion level is reached! + if (this.depth < 0) { + throw new MergeError('Invalid maximum "depth" merge option value', { + cause: { + options: this + } + }); + } + + // Resolve merge callback + this.callback = (options && typeof options == 'function') + ? options + : makeDefaultMergeCallback() + + // Resolve skip callback + if (typeof this.skip != 'function') { + this.skip = makeDefaultSkipCallback(this.skip as PropertyKey[]); + } + } + + /** + * Create new default merge options from given options + * + * @param {MergeCallback | MergeOptions} [options] + * + * @return {DefaultMergeOptions} + * + * @throws {MergeError} + */ + public static from(options?: MergeCallback | MergeOptions): Readonly + { + const resoled = new this(options); + + return Object.freeze(resoled) as Readonly; + } +} \ No newline at end of file diff --git a/packages/support/src/objects/merge/Merger.ts b/packages/support/src/objects/merge/Merger.ts new file mode 100644 index 00000000..194c25b4 --- /dev/null +++ b/packages/support/src/objects/merge/Merger.ts @@ -0,0 +1,285 @@ +import type { + Cloneable, + MergeCallback, + MergeOptions, + MergeSourceInfo, + NextCallback, + SkipKeyCallback, + ObjectsMerger +} from "@aedart/contracts/support/objects"; +import DefaultMergeOptions from "./DefaultMergeOptions"; +import { MergeError } from "../exceptions"; +import { getErrorMessage } from "@aedart/support/exceptions"; +import { isCloneable } from "@aedart/support/objects"; +import { descTag } from "@aedart/support/misc"; + +/** + * Objects Merger + * + * @see ObjectsMerger + */ +export default class Merger implements ObjectsMerger +{ + /** + * The merge options to be applied + * + * @private + * + * @type {Readonly} + */ + #options: Readonly; + + /** + * Callback to perform the merging of nested objects. + * + * @private + * + * @type {NextCallback} + */ + readonly #next: NextCallback; + + /** + * Create a new objects merger instance + * + * @param {MergeCallback | MergeOptions} [options] + * + * @throws {MergeError} + */ + public constructor(options?: MergeCallback | MergeOptions) { + // @ts-expect-error Need to init options, however they are resolved via "using". + this.#options = null; + this.#next = this.merge; + + this.using(options); + } + + /** + * Returns the merge options that are to be applied + * + * @return {Readonly} + */ + get options(): Readonly + { + return this.#options; + } + + /** + * Returns the "next" callback that performs merging of nested objects. + */ + get nextCallback(): NextCallback + { + return this.#next; + } + + /** + * Use the following merge options or merge callback + * + * @param {MergeCallback | MergeOptions} [options] + * + * @return {this} + * + * @throws {MergeError} + */ + public using(options?: MergeCallback | MergeOptions): this + { + this.#options = this.resolveOptions(options); + + return this; + } + + public of(...sources: object[]): object + { + try { + return this.nextCallback(sources, this.options, 0); + } catch (error) { + if (error instanceof MergeError) { + // @ts-expect-error Error SHOULD have a cause object set - support by all browsers now! + error.cause.sources = sources; + // @ts-expect-error Error SHOULD have a cause object set - support by all browsers now! + error.cause.options = this.options; + + throw error; + } + + const reason: string = getErrorMessage(error); + throw new MergeError(`Unable to merge objects: ${reason}`, { + cause: { + previous: error, + sources: sources, + options: this.options + } + }); + } + } + + /** + * Merge given source objects into a single object + * + * @param {object[]} sources + * @param {Readonly} options + * @param {number} [depth] Current recursion depth + * + * @return {object} + * + * @throws {MergeError} + */ + public merge(sources: object[], options: Readonly, depth: number = 0): object + { + // Abort if maximum depth has been reached + this.assertMaxDepthNotExceeded(depth, sources, options); + + // Resolve callbacks to apply + const nextCallback: NextCallback = this.nextCallback.bind(this); + const skipCallback: SkipKeyCallback = (options.skip as SkipKeyCallback).bind(this); + const mergeCallback: MergeCallback = (options.callback as MergeCallback).bind(this); + + // Loop through the sources and merge them into a single object + return sources.reduce((result: object, source: object, index: number) => { + // Abort if source is invalid... + this.assertSourceObject(source, index, depth); + + // If allowed and source implements "Cloneable" interface, favour "clone()" method's resulting object. + const resolved: object = this.resolveSourceObject(source, options); + + // Loop through all the source's properties, including symbols + const keys: PropertyKey[] = Reflect.ownKeys(resolved); + for (const key of keys){ + // Skip key if needed ... + if (skipCallback(key, resolved, result)) { + continue; + } + + // Resolve the value via callback and set it in resulting object. + // @ts-expect-error Safe to set the value in result object! + result[key] = mergeCallback( + { + result, + key, + // @ts-expect-error Value can be of any type + value: resolved[key], + source: resolved, + sourceIndex: index, + depth: depth, + } as MergeSourceInfo, + nextCallback, + options + ); + } + + return result; + }, Object.create(null)); + } + + /** + * Resolves the source object + * + * @param {object} source + * @param {MergeOptions} options + * + * @protected + * + * @return {object} + */ + protected resolveSourceObject(source: object, options: MergeOptions): object + { + let output: object = source; + if (options.useCloneable && isCloneable(source)) { + output = this.cloneSource(source as Cloneable); + } + + return output; + } + + /** + * Invokes the "clone()" method on given cloneable object + * + * @param {Cloneable} source + * + * @protected + * + * @return {object} + * + * @return {MergeError} If unable to + */ + protected cloneSource(source: Cloneable): object + { + const clone: object = source.clone(); + + // Abort if resulting value from "clone()" is not a valid value... + if (!clone || typeof clone != 'object' || Array.isArray(clone)) { + throw new MergeError(`Expected clone() method to return object for source, ${descTag(clone)} was returned`, { + cause: { + source: source, + clone: clone, + } + }); + } + + return clone; + } + + /** + * Resolves provided merge options + * + * @param {MergeCallback | MergeOptions} [options] + * + * @protected + * + * @return {Readonly} + * + * @throws {MergeError} + */ + protected resolveOptions(options?: MergeCallback | MergeOptions): Readonly + { + return DefaultMergeOptions.from(options); + } + + /** + * Assert that current recursion depth has now exceeded the maximum depth + * + * @param {number} currentDepth + * @param {object[]} sources + * @param {MergeOptions} [options] Defaults to this Merger's options when none given + * + * @protected + * + * @throws {MergeError} + */ + protected assertMaxDepthNotExceeded(currentDepth: number, sources: object[], options?: MergeOptions): void + { + const max: number | undefined = options?.depth || this.options.depth; + + if (max && currentDepth > max) { + throw new MergeError(`Maximum merge depth (${max}) has been exceeded`, { + cause: { + source: sources, + depth: currentDepth + } + }); + } + } + + /** + * Assert given source is a valid object + * + * @param {unknown} source + * @param {number} index + * @param {number} currentDepth + * + * @protected + * + * @throws {MergeError} + */ + protected assertSourceObject(source: unknown, index: number, currentDepth: number): void + { + if (!source || typeof source != 'object' || Array.isArray(source)) { + throw new MergeError(`Unable to merge source of invalid type "${descTag(source)}" (source index: ${index})`, { + cause: { + source: source, + index: index, + depth: currentDepth + } + }); + } + } +} \ No newline at end of file diff --git a/packages/support/src/objects/merge/canCloneUsingStructuredClone.ts b/packages/support/src/objects/merge/canCloneUsingStructuredClone.ts new file mode 100644 index 00000000..da1a5f08 --- /dev/null +++ b/packages/support/src/objects/merge/canCloneUsingStructuredClone.ts @@ -0,0 +1,41 @@ +import type { Constructor } from "@aedart/contracts"; +import { TYPED_ARRAY_PROTOTYPE } from "@aedart/contracts/support/reflections"; + +/** + * Determine if an object value can be cloned via `structuredClone()` + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/structuredClone + * + * @internal + * + * @param {object} value + * + * @return {boolean} + */ +export function canCloneUsingStructuredClone(value: object): boolean +{ + const supported = [ + // Array, // Handled by array, with evt. array value merges + ArrayBuffer, + Boolean, + DataView, + Date, + Error, + Map, + Number, + // Object, // Handled by "basic" objects merging... + // (Primitive Types), // Also handled elsewhere... + RegExp, + Set, + String, + TYPED_ARRAY_PROTOTYPE + ]; + + for (const constructor of supported) { + if (value instanceof (constructor as Constructor)) { + return true; + } + } + + return false; +} \ No newline at end of file diff --git a/packages/support/src/objects/merge/index.ts b/packages/support/src/objects/merge/index.ts new file mode 100644 index 00000000..5f3a15bb --- /dev/null +++ b/packages/support/src/objects/merge/index.ts @@ -0,0 +1,10 @@ +import DefaultMergeOptions from "./DefaultMergeOptions"; +import Merger from "./Merger"; +export { + DefaultMergeOptions, + Merger +} + +export * from './canCloneUsingStructuredClone'; +export * from './makeDefaultMergeCallback'; +export * from './makeDefaultSkipCallback'; \ No newline at end of file diff --git a/packages/support/src/objects/merge/makeDefaultMergeCallback.ts b/packages/support/src/objects/merge/makeDefaultMergeCallback.ts new file mode 100644 index 00000000..bc79cae8 --- /dev/null +++ b/packages/support/src/objects/merge/makeDefaultMergeCallback.ts @@ -0,0 +1,137 @@ +import {MergeCallback, MergeOptions, MergeSourceInfo, NextCallback} from "@aedart/contracts/support/objects"; +import {isConcatSpreadable, isSafeArrayLike, merge as mergeArrays} from "@aedart/support/arrays"; +import {canCloneUsingStructuredClone, MergeError} from "@aedart/support/objects"; +import {isWeakKind} from "@aedart/support/reflections"; +import {descTag} from "@aedart/support/misc"; + +/** + * Returns a new "default" merge callback + * + * @return {MergeCallback} + */ +export function makeDefaultMergeCallback(): MergeCallback +{ + return function(target: MergeSourceInfo, next: NextCallback, options: Readonly): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + const { + result, + key, + value, + source, + sourceIndex, + depth + } = target; + + // Determine if result contains key + const hasExisting: boolean = Reflect.has(result, key); + + // The existing value, if any + // @ts-expect-error Existing value can be of any type here... + const existingValue: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ = result[key]; + + // Determine the type and resolve value based on it... + const type: string = typeof value; + + switch (type) { + + // -------------------------------------------------------------------------------------------------------- // + // Primitives + // @see https://developer.mozilla.org/en-US/docs/Glossary/Primitive + + case 'undefined': + // Do not overwrite existing value with `undefined`, if options do not allow it... + if (value === undefined + && options.overwriteWithUndefined === false + && hasExisting + && existingValue !== undefined + ) { + return existingValue; + } + + return value; + + case 'string': + case 'number': + case 'bigint': + case 'boolean': + case 'symbol': + return value; + + // -------------------------------------------------------------------------------------------------------- // + // Functions + + case 'function': + return value; + + // -------------------------------------------------------------------------------------------------------- // + // Null, Arrays and Objects + case 'object': + // Null (primitive) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + if (value === null) { + return value; + } + + // Arrays - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + const isArray: boolean = Array.isArray(value); /* eslint-disable-line no-case-declarations */ + + if (isArray || isConcatSpreadable(value) || isSafeArrayLike(value)) { + + // If required to merge with existing value, if one exists... + if (options.mergeArrays === true + && hasExisting + && (isArray || Array.isArray(existingValue)) + ) { + // If either existing or new value is of the type array, merge values into + // a new array. + return mergeArrays(existingValue, value); + } else if (isArray) { + // When not requested merged, just overwrite existing value with a new array, + // if new value is an array. + return mergeArrays(value); + } + + // For concat spreadable objects or array-like objects, the "basic object" merge logic + // will deal with them. + } + + // Objects (of "native" kind) - - - - - - - - - - - - - - - - - - - - - - - - + // Clone the object of a "native" kind value, if supported. + if (canCloneUsingStructuredClone(value)) { + return structuredClone(value); + } + + // Objects (WeakRef, WeakMap and WeakSet) - - - - - - - - - - - - - - - - - - + // "Weak Reference" kind of objects cannot, nor should they, be cloned. + if (isWeakKind(value)) { + return value; + } + + // Objects (basic)- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Merge with existing, if existing value is not null... + if (hasExisting + && typeof existingValue == 'object' + && existingValue !== null + && !(Array.isArray(existingValue)) + ) { + return next([ existingValue, value ], options, depth + 1); + } + + // Otherwise, create a new object and merge it. + return next([ Object.create(null), value ], options, depth + 1); + + // -------------------------------------------------------------------------------------------------------- // + // If for some reason this point is reached, it means that we are unable to merge "something". + default: + throw new MergeError(`Unable to merge value of type ${type} (${descTag(value)}) at source index ${sourceIndex}`, { + cause: { + key: key, + value: value, + source: source, + sourceIndex: sourceIndex, + depth: depth, + options: options + } + }); + } + } +} \ No newline at end of file diff --git a/packages/support/src/objects/merge/makeDefaultSkipCallback.ts b/packages/support/src/objects/merge/makeDefaultSkipCallback.ts new file mode 100644 index 00000000..c2151d8f --- /dev/null +++ b/packages/support/src/objects/merge/makeDefaultSkipCallback.ts @@ -0,0 +1,19 @@ +import type { SkipKeyCallback } from "@aedart/contracts/support/objects"; + +/** + * Returns a new "default" skip callback for given keys + * + * @param {PropertyKey[]} keys + * + * @return {SkipKeyCallback} + */ +export function makeDefaultSkipCallback(keys: PropertyKey[]): SkipKeyCallback +{ + return ( + key: PropertyKey, + source: object, + result: object /* eslint-disable-line @typescript-eslint/no-unused-vars */ + ) => { + return keys.includes(key) && Reflect.has(source, key); + } +} \ No newline at end of file diff --git a/packages/support/src/objects/merger.ts b/packages/support/src/objects/merger.ts new file mode 100644 index 00000000..8a41f879 --- /dev/null +++ b/packages/support/src/objects/merger.ts @@ -0,0 +1,18 @@ +import type { + MergeOptions, + MergeCallback, + ObjectsMerger +} from "@aedart/contracts/support/objects"; +import Merger from "./merge/Merger"; + +/** + * Returns a new objects merger instance + * + * @param {MergeCallback | MergeOptions} [options] + * + * @return {ObjectsMerger} + */ +export function merger(options?: MergeCallback | MergeOptions): ObjectsMerger +{ + return new Merger(options); +} \ No newline at end of file diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js index 1ebbe1b4..f9e55455 100644 --- a/tests/browser/packages/support/objects/merge.test.js +++ b/tests/browser/packages/support/objects/merge.test.js @@ -2,6 +2,7 @@ import { DEFAULT_MERGE_SKIP_KEYS } from "@aedart/contracts/support/objects"; import { TYPED_ARRAY_PROTOTYPE } from "@aedart/contracts/support/reflections"; import { merge, + merger, MergeError } from "@aedart/support/objects"; @@ -36,7 +37,8 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ]); + // const result = merge([a, b]); + const result = merger().of(a, b); // Debug //console.log('result', result); @@ -105,7 +107,8 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ], (result, key, value) => { + const result = merge([ a, b ], (target, next, options) => { + const { key, value } = target; if (key === 'b') { return value + 1; } From 19f4f6c845401eed0a7899f038e9c8f9b6cef987 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 21 Feb 2024 16:07:19 +0100 Subject: [PATCH 198/424] Change release notes --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ffe9ffb..52150b10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,12 +21,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `hasPrototypeProperty()` and `assertHasPrototypeProperty()` in `@aedart/support/reflections`. * `getParentOfClass()` and `getAllParentsOfClass()` in `@aedart/support/reflections`. * `getClassPropertyDescriptor()` and `getClassPropertyDescriptors()` in `@aedart/support/reflections`. +* `isWeakKind()` in `@aedart/support/reflections`. * `getConstructorName()` and `getNameOrDesc()` in `@aedart/support/reflections`. * `isSubclass()` in `@aedart/support/reflections`. * `classOwnKeys()` in `@aedart/support/reflections`. -* `merge()` in `@aedart/support/objects`. -* `merge()`, `isTypedArray()`, `isArrayLike` and `isConcatSpreadable()` in `@aedart/support/arrays`. -* `isCloneable()` in `@aedart/support/objects`. +* `merge()`, `merger()`, and `isCloneable()` in `@aedart/support/objects`. +* `merge()`, `isTypedArray()`, `isArrayLike()`, `isSafeArrayLike()`, `isTypedArray()` and `isConcatSpreadable()` in `@aedart/support/arrays`. ## [0.8.0] - 2024-02-12 From 92d065a3ea71d772baf14b111849920b842c9bcf Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Wed, 21 Feb 2024 20:29:59 +0100 Subject: [PATCH 199/424] Refactor skip keys logic - always skip dangerous keys Regardless of what keys might be provided, dangerous keys should always be skipped. Developers should not be allowed to circumvent this - at least not in any easy way! --- packages/contracts/src/support/objects/index.ts | 12 ++++++++++++ .../src/support/objects/merge/MergeOptions.ts | 4 ++-- .../src/support/objects/merge/index.ts | 7 ------- .../src/objects/merge/DefaultMergeOptions.ts | 11 ++++------- packages/support/src/objects/merge/Merger.ts | 3 ++- .../packages/support/objects/merge.test.js | 17 ++++++++++++----- 6 files changed, 32 insertions(+), 22 deletions(-) diff --git a/packages/contracts/src/support/objects/index.ts b/packages/contracts/src/support/objects/index.ts index a46f11e2..b3ace525 100644 --- a/packages/contracts/src/support/objects/index.ts +++ b/packages/contracts/src/support/objects/index.ts @@ -5,6 +5,18 @@ */ export const SUPPORT_OBJECTS: unique symbol = Symbol('@aedart/contracts/support/objects'); +/** + * Properties that are considered dangerous and should be avoided when merging + * objects or assigning properties. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf#description + * @see https://cheatsheetseries.owasp.org/cheatsheets/Prototype_Pollution_Prevention_Cheat_Sheet.html + * @see https://medium.com/@king.amit95/prototype-pollution-a-deeper-inspection-82a226796966 + * + * @type {PropertyKey[]} + */ +export const DANGEROUS_PROPERTIES: PropertyKey[] = [ '__proto__' ]; + import Cloneable from "./Cloneable"; export { type Cloneable, diff --git a/packages/contracts/src/support/objects/merge/MergeOptions.ts b/packages/contracts/src/support/objects/merge/MergeOptions.ts index c71df5b3..ddddc93f 100644 --- a/packages/contracts/src/support/objects/merge/MergeOptions.ts +++ b/packages/contracts/src/support/objects/merge/MergeOptions.ts @@ -23,8 +23,8 @@ export default interface MergeOptions /** * Property Keys that must not be merged. * - * **Note**: _Defaults to [DEFAULT_MERGE_SKIP_KEYS]{@link import('@aedart/contracts/support/objects').DEFAULT_MERGE_SKIP_KEYS} - * when not specified._ + * **Note**: [DANGEROUS_PROPERTIES]{@link import('@aedart/contracts/support/objects').DANGEROUS_PROPERTIES} + * are always skipped, regardless of specified keys._ * * **Callback**: _A callback can be specified to determine if a given key, * in a source object should be skipped._ diff --git a/packages/contracts/src/support/objects/merge/index.ts b/packages/contracts/src/support/objects/merge/index.ts index 42dbc0e5..d99081ee 100644 --- a/packages/contracts/src/support/objects/merge/index.ts +++ b/packages/contracts/src/support/objects/merge/index.ts @@ -1,10 +1,3 @@ -/** - * Default property keys to be skipped when merging objects - * - * @type {PropertyKey[]} - */ -export const DEFAULT_MERGE_SKIP_KEYS: PropertyKey[] = [ 'prototype', '__proto__' ]; - /** * Default maximum merge depth * diff --git a/packages/support/src/objects/merge/DefaultMergeOptions.ts b/packages/support/src/objects/merge/DefaultMergeOptions.ts index 49b9258c..3436a418 100644 --- a/packages/support/src/objects/merge/DefaultMergeOptions.ts +++ b/packages/support/src/objects/merge/DefaultMergeOptions.ts @@ -3,10 +3,7 @@ import type { MergeOptions, SkipKeyCallback } from "@aedart/contracts/support/objects"; -import { - DEFAULT_MAX_MERGE_DEPTH, - DEFAULT_MERGE_SKIP_KEYS -} from "@aedart/contracts/support/objects"; +import { DEFAULT_MAX_MERGE_DEPTH } from "@aedart/contracts/support/objects"; import { MergeError } from "../exceptions"; import { makeDefaultMergeCallback } from "./makeDefaultMergeCallback"; import { makeDefaultSkipCallback } from "./makeDefaultSkipCallback"; @@ -33,8 +30,8 @@ export default class DefaultMergeOptions implements MergeOptions /** * Property Keys that must not be merged. * - * **Note**: _Defaults to [DEFAULT_MERGE_SKIP_KEYS]{@link import('@aedart/contracts/support/objects').DEFAULT_MERGE_SKIP_KEYS} - * when not specified._ + * **Note**: [DANGEROUS_PROPERTIES]{@link import('@aedart/contracts/support/objects').DANGEROUS_PROPERTIES} + * are always skipped, regardless of specified keys._ * * **Callback**: _A callback can be specified to determine if a given key, * in a source object should be skipped._ @@ -53,7 +50,7 @@ export default class DefaultMergeOptions implements MergeOptions * * @type {PropertyKey[] | SkipKeyCallback} */ - skip: PropertyKey[] | SkipKeyCallback = DEFAULT_MERGE_SKIP_KEYS; + skip: PropertyKey[] | SkipKeyCallback = []; /** * Flag, overwrite property values with `undefined`. diff --git a/packages/support/src/objects/merge/Merger.ts b/packages/support/src/objects/merge/Merger.ts index 194c25b4..8b499005 100644 --- a/packages/support/src/objects/merge/Merger.ts +++ b/packages/support/src/objects/merge/Merger.ts @@ -7,6 +7,7 @@ import type { SkipKeyCallback, ObjectsMerger } from "@aedart/contracts/support/objects"; +import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; import DefaultMergeOptions from "./DefaultMergeOptions"; import { MergeError } from "../exceptions"; import { getErrorMessage } from "@aedart/support/exceptions"; @@ -145,7 +146,7 @@ export default class Merger implements ObjectsMerger const keys: PropertyKey[] = Reflect.ownKeys(resolved); for (const key of keys){ // Skip key if needed ... - if (skipCallback(key, resolved, result)) { + if (DANGEROUS_PROPERTIES.includes(key) || skipCallback(key, resolved, result)) { continue; } diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js index f9e55455..e28bfd53 100644 --- a/tests/browser/packages/support/objects/merge.test.js +++ b/tests/browser/packages/support/objects/merge.test.js @@ -1,4 +1,4 @@ -import { DEFAULT_MERGE_SKIP_KEYS } from "@aedart/contracts/support/objects"; +import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; import { TYPED_ARRAY_PROTOTYPE } from "@aedart/contracts/support/reflections"; import { merge, @@ -133,7 +133,7 @@ describe('@aedart/support/objects', () => { .toBe(b['b'] + 1); }); - it('skips default keys', () => { + it('skips dangerous keys', () => { const a = { 'foo': 'bar' }; @@ -151,9 +151,9 @@ describe('@aedart/support/objects', () => { // Debug // console.log('result', result); - for (const key of DEFAULT_MERGE_SKIP_KEYS) { + for (const key of DANGEROUS_PROPERTIES) { expect(Reflect.has(result, key)) - .withContext(`Default skip key (${key}) is not skipped`) + .withContext(`Dangerous key (${key}) is not skipped`) .toBeFalse(); } }); @@ -163,7 +163,8 @@ describe('@aedart/support/objects', () => { 'foo': 'bar' }; const b = { - 'bar': 'foo' + 'bar': 'foo', + __proto__: { 'admin': true } }; // --------------------------------------------------------------------- // @@ -180,6 +181,12 @@ describe('@aedart/support/objects', () => { expect(Reflect.has(result, 'foo')) .withContext('Skipped key is in output') .toBeFalse(); + + for (const key of DANGEROUS_PROPERTIES) { + expect(Reflect.has(result, key)) + .withContext(`Dangerous key (${key}) is not skipped`) + .toBeFalse(); + } }); it('can skip keys via callback', () => { From 931acead70a1c8c4135f9b61164eb5fb42d6e010 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Wed, 21 Feb 2024 20:30:05 +0100 Subject: [PATCH 200/424] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52150b10..06b87763 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `LogicalError` and `AbstractClassError` exceptions in `@aedart/support/exceptions`. * `getErrorMessage()` in `@aedart/support/exceptions`. * `FUNCTION_PROTOTYPE` and `TYPED_ARRAY_PROTOTYPE` constants in `@aedart/contracts/support/reflections`. +* `DANGEROUS_PROPERTIES` constant in `@aedart/contracts/support/objects`. * `hasPrototypeProperty()` and `assertHasPrototypeProperty()` in `@aedart/support/reflections`. * `getParentOfClass()` and `getAllParentsOfClass()` in `@aedart/support/reflections`. * `getClassPropertyDescriptor()` and `getClassPropertyDescriptors()` in `@aedart/support/reflections`. From 75a3a449144ea5929887ac7b29c635a6143d271e Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Wed, 21 Feb 2024 20:31:08 +0100 Subject: [PATCH 201/424] Rename makeDefaultSkipCallback to makeSkipCallback --- packages/support/src/objects/merge/DefaultMergeOptions.ts | 4 ++-- packages/support/src/objects/merge/index.ts | 2 +- .../merge/{makeDefaultSkipCallback.ts => makeSkipCallback.ts} | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename packages/support/src/objects/merge/{makeDefaultSkipCallback.ts => makeSkipCallback.ts} (73%) diff --git a/packages/support/src/objects/merge/DefaultMergeOptions.ts b/packages/support/src/objects/merge/DefaultMergeOptions.ts index 3436a418..63a98aaf 100644 --- a/packages/support/src/objects/merge/DefaultMergeOptions.ts +++ b/packages/support/src/objects/merge/DefaultMergeOptions.ts @@ -6,7 +6,7 @@ import type { import { DEFAULT_MAX_MERGE_DEPTH } from "@aedart/contracts/support/objects"; import { MergeError } from "../exceptions"; import { makeDefaultMergeCallback } from "./makeDefaultMergeCallback"; -import { makeDefaultSkipCallback } from "./makeDefaultSkipCallback"; +import { makeSkipCallback } from "./makeSkipCallback"; /** * Default Merge Options @@ -189,7 +189,7 @@ export default class DefaultMergeOptions implements MergeOptions // Resolve skip callback if (typeof this.skip != 'function') { - this.skip = makeDefaultSkipCallback(this.skip as PropertyKey[]); + this.skip = makeSkipCallback(this.skip as PropertyKey[]); } } diff --git a/packages/support/src/objects/merge/index.ts b/packages/support/src/objects/merge/index.ts index 5f3a15bb..fbf78251 100644 --- a/packages/support/src/objects/merge/index.ts +++ b/packages/support/src/objects/merge/index.ts @@ -7,4 +7,4 @@ export { export * from './canCloneUsingStructuredClone'; export * from './makeDefaultMergeCallback'; -export * from './makeDefaultSkipCallback'; \ No newline at end of file +export * from './makeSkipCallback'; \ No newline at end of file diff --git a/packages/support/src/objects/merge/makeDefaultSkipCallback.ts b/packages/support/src/objects/merge/makeSkipCallback.ts similarity index 73% rename from packages/support/src/objects/merge/makeDefaultSkipCallback.ts rename to packages/support/src/objects/merge/makeSkipCallback.ts index c2151d8f..8a7049f0 100644 --- a/packages/support/src/objects/merge/makeDefaultSkipCallback.ts +++ b/packages/support/src/objects/merge/makeSkipCallback.ts @@ -1,13 +1,13 @@ import type { SkipKeyCallback } from "@aedart/contracts/support/objects"; /** - * Returns a new "default" skip callback for given keys + * Returns a new skip callback for given property keys * * @param {PropertyKey[]} keys * * @return {SkipKeyCallback} */ -export function makeDefaultSkipCallback(keys: PropertyKey[]): SkipKeyCallback +export function makeSkipCallback(keys: PropertyKey[]): SkipKeyCallback { return ( key: PropertyKey, From b306b3902cc23b16427ee33a5acc15d7c23fc0e7 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Wed, 21 Feb 2024 20:36:28 +0100 Subject: [PATCH 202/424] Refactor, rename makeDefaultMergeCallback to defaultMergeCallback Seems better --- .../src/objects/merge/DefaultMergeOptions.ts | 4 +- .../src/objects/merge/defaultMergeCallback.ts | 134 +++++++++++++++++ packages/support/src/objects/merge/index.ts | 2 +- .../objects/merge/makeDefaultMergeCallback.ts | 137 ------------------ 4 files changed, 137 insertions(+), 140 deletions(-) create mode 100644 packages/support/src/objects/merge/defaultMergeCallback.ts delete mode 100644 packages/support/src/objects/merge/makeDefaultMergeCallback.ts diff --git a/packages/support/src/objects/merge/DefaultMergeOptions.ts b/packages/support/src/objects/merge/DefaultMergeOptions.ts index 63a98aaf..187748a7 100644 --- a/packages/support/src/objects/merge/DefaultMergeOptions.ts +++ b/packages/support/src/objects/merge/DefaultMergeOptions.ts @@ -5,7 +5,7 @@ import type { } from "@aedart/contracts/support/objects"; import { DEFAULT_MAX_MERGE_DEPTH } from "@aedart/contracts/support/objects"; import { MergeError } from "../exceptions"; -import { makeDefaultMergeCallback } from "./makeDefaultMergeCallback"; +import { defaultMergeCallback } from "./defaultMergeCallback"; import { makeSkipCallback } from "./makeSkipCallback"; /** @@ -185,7 +185,7 @@ export default class DefaultMergeOptions implements MergeOptions // Resolve merge callback this.callback = (options && typeof options == 'function') ? options - : makeDefaultMergeCallback() + : defaultMergeCallback // Resolve skip callback if (typeof this.skip != 'function') { diff --git a/packages/support/src/objects/merge/defaultMergeCallback.ts b/packages/support/src/objects/merge/defaultMergeCallback.ts new file mode 100644 index 00000000..87945b22 --- /dev/null +++ b/packages/support/src/objects/merge/defaultMergeCallback.ts @@ -0,0 +1,134 @@ +import { MergeCallback, MergeOptions, MergeSourceInfo, NextCallback } from "@aedart/contracts/support/objects"; +import { isConcatSpreadable, isSafeArrayLike, merge as mergeArrays } from "@aedart/support/arrays"; +import { canCloneUsingStructuredClone, MergeError } from "@aedart/support/objects"; +import { isWeakKind } from "@aedart/support/reflections"; +import { descTag } from "@aedart/support/misc"; + +/** + * Returns a new "default" merge callback + * + * @return {MergeCallback} + */ +export const defaultMergeCallback: MergeCallback = function(target: MergeSourceInfo, next: NextCallback, options: Readonly): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +{ + const { + result, + key, + value, + source, + sourceIndex, + depth + } = target; + + // Determine if result contains key + const hasExisting: boolean = Reflect.has(result, key); + + // The existing value, if any + // @ts-expect-error Existing value can be of any type here... + const existingValue: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ = result[key]; + + // Determine the type and resolve value based on it... + const type: string = typeof value; + + switch (type) { + + // -------------------------------------------------------------------------------------------------------- // + // Primitives + // @see https://developer.mozilla.org/en-US/docs/Glossary/Primitive + + case 'undefined': + // Do not overwrite existing value with `undefined`, if options do not allow it... + if (value === undefined + && options.overwriteWithUndefined === false + && hasExisting + && existingValue !== undefined + ) { + return existingValue; + } + + return value; + + case 'string': + case 'number': + case 'bigint': + case 'boolean': + case 'symbol': + return value; + + // -------------------------------------------------------------------------------------------------------- // + // Functions + + case 'function': + return value; + + // -------------------------------------------------------------------------------------------------------- // + // Null, Arrays and Objects + case 'object': + // Null (primitive) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + if (value === null) { + return value; + } + + // Arrays - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + const isArray: boolean = Array.isArray(value); /* eslint-disable-line no-case-declarations */ + + if (isArray || isConcatSpreadable(value) || isSafeArrayLike(value)) { + + // If required to merge with existing value, if one exists... + if (options.mergeArrays === true + && hasExisting + && (isArray || Array.isArray(existingValue)) + ) { + // If either existing or new value is of the type array, merge values into + // a new array. + return mergeArrays(existingValue, value); + } else if (isArray) { + // When not requested merged, just overwrite existing value with a new array, + // if new value is an array. + return mergeArrays(value); + } + + // For concat spreadable objects or array-like objects, the "basic object" merge logic + // will deal with them. + } + + // Objects (of "native" kind) - - - - - - - - - - - - - - - - - - - - - - - - + // Clone the object of a "native" kind value, if supported. + if (canCloneUsingStructuredClone(value)) { + return structuredClone(value); + } + + // Objects (WeakRef, WeakMap and WeakSet) - - - - - - - - - - - - - - - - - - + // "Weak Reference" kind of objects cannot, nor should they, be cloned. + if (isWeakKind(value)) { + return value; + } + + // Objects (basic)- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Merge with existing, if existing value is not null... + if (hasExisting + && typeof existingValue == 'object' + && existingValue !== null + && !(Array.isArray(existingValue)) + ) { + return next([ existingValue, value ], options, depth + 1); + } + + // Otherwise, create a new object and merge it. + return next([ Object.create(null), value ], options, depth + 1); + + // -------------------------------------------------------------------------------------------------------- // + // If for some reason this point is reached, it means that we are unable to merge "something". + default: + throw new MergeError(`Unable to merge value of type ${type} (${descTag(value)}) at source index ${sourceIndex}`, { + cause: { + key: key, + value: value, + source: source, + sourceIndex: sourceIndex, + depth: depth, + options: options + } + }); + } +}; \ No newline at end of file diff --git a/packages/support/src/objects/merge/index.ts b/packages/support/src/objects/merge/index.ts index fbf78251..9086a88f 100644 --- a/packages/support/src/objects/merge/index.ts +++ b/packages/support/src/objects/merge/index.ts @@ -6,5 +6,5 @@ export { } export * from './canCloneUsingStructuredClone'; -export * from './makeDefaultMergeCallback'; +export * from './defaultMergeCallback'; export * from './makeSkipCallback'; \ No newline at end of file diff --git a/packages/support/src/objects/merge/makeDefaultMergeCallback.ts b/packages/support/src/objects/merge/makeDefaultMergeCallback.ts deleted file mode 100644 index bc79cae8..00000000 --- a/packages/support/src/objects/merge/makeDefaultMergeCallback.ts +++ /dev/null @@ -1,137 +0,0 @@ -import {MergeCallback, MergeOptions, MergeSourceInfo, NextCallback} from "@aedart/contracts/support/objects"; -import {isConcatSpreadable, isSafeArrayLike, merge as mergeArrays} from "@aedart/support/arrays"; -import {canCloneUsingStructuredClone, MergeError} from "@aedart/support/objects"; -import {isWeakKind} from "@aedart/support/reflections"; -import {descTag} from "@aedart/support/misc"; - -/** - * Returns a new "default" merge callback - * - * @return {MergeCallback} - */ -export function makeDefaultMergeCallback(): MergeCallback -{ - return function(target: MergeSourceInfo, next: NextCallback, options: Readonly): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ - { - const { - result, - key, - value, - source, - sourceIndex, - depth - } = target; - - // Determine if result contains key - const hasExisting: boolean = Reflect.has(result, key); - - // The existing value, if any - // @ts-expect-error Existing value can be of any type here... - const existingValue: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ = result[key]; - - // Determine the type and resolve value based on it... - const type: string = typeof value; - - switch (type) { - - // -------------------------------------------------------------------------------------------------------- // - // Primitives - // @see https://developer.mozilla.org/en-US/docs/Glossary/Primitive - - case 'undefined': - // Do not overwrite existing value with `undefined`, if options do not allow it... - if (value === undefined - && options.overwriteWithUndefined === false - && hasExisting - && existingValue !== undefined - ) { - return existingValue; - } - - return value; - - case 'string': - case 'number': - case 'bigint': - case 'boolean': - case 'symbol': - return value; - - // -------------------------------------------------------------------------------------------------------- // - // Functions - - case 'function': - return value; - - // -------------------------------------------------------------------------------------------------------- // - // Null, Arrays and Objects - case 'object': - // Null (primitive) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if (value === null) { - return value; - } - - // Arrays - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const isArray: boolean = Array.isArray(value); /* eslint-disable-line no-case-declarations */ - - if (isArray || isConcatSpreadable(value) || isSafeArrayLike(value)) { - - // If required to merge with existing value, if one exists... - if (options.mergeArrays === true - && hasExisting - && (isArray || Array.isArray(existingValue)) - ) { - // If either existing or new value is of the type array, merge values into - // a new array. - return mergeArrays(existingValue, value); - } else if (isArray) { - // When not requested merged, just overwrite existing value with a new array, - // if new value is an array. - return mergeArrays(value); - } - - // For concat spreadable objects or array-like objects, the "basic object" merge logic - // will deal with them. - } - - // Objects (of "native" kind) - - - - - - - - - - - - - - - - - - - - - - - - - // Clone the object of a "native" kind value, if supported. - if (canCloneUsingStructuredClone(value)) { - return structuredClone(value); - } - - // Objects (WeakRef, WeakMap and WeakSet) - - - - - - - - - - - - - - - - - - - // "Weak Reference" kind of objects cannot, nor should they, be cloned. - if (isWeakKind(value)) { - return value; - } - - // Objects (basic)- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Merge with existing, if existing value is not null... - if (hasExisting - && typeof existingValue == 'object' - && existingValue !== null - && !(Array.isArray(existingValue)) - ) { - return next([ existingValue, value ], options, depth + 1); - } - - // Otherwise, create a new object and merge it. - return next([ Object.create(null), value ], options, depth + 1); - - // -------------------------------------------------------------------------------------------------------- // - // If for some reason this point is reached, it means that we are unable to merge "something". - default: - throw new MergeError(`Unable to merge value of type ${type} (${descTag(value)}) at source index ${sourceIndex}`, { - cause: { - key: key, - value: value, - source: source, - sourceIndex: sourceIndex, - depth: depth, - options: options - } - }); - } - } -} \ No newline at end of file From 62eb54372b50f68b2e7267718a0538f26f8c0437 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Wed, 21 Feb 2024 20:36:58 +0100 Subject: [PATCH 203/424] Fix description --- packages/support/src/objects/merge/defaultMergeCallback.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/support/src/objects/merge/defaultMergeCallback.ts b/packages/support/src/objects/merge/defaultMergeCallback.ts index 87945b22..a2fb4e48 100644 --- a/packages/support/src/objects/merge/defaultMergeCallback.ts +++ b/packages/support/src/objects/merge/defaultMergeCallback.ts @@ -5,9 +5,9 @@ import { isWeakKind } from "@aedart/support/reflections"; import { descTag } from "@aedart/support/misc"; /** - * Returns a new "default" merge callback + * The default merge callback * - * @return {MergeCallback} + * @type {MergeCallback} */ export const defaultMergeCallback: MergeCallback = function(target: MergeSourceInfo, next: NextCallback, options: Readonly): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { From 323705f99dd7dfac29b7671b581611c7a2b8d24a Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Wed, 21 Feb 2024 20:56:46 +0100 Subject: [PATCH 204/424] Add isKeySafe() and isKeyUnsafe() util methods --- packages/support/src/reflections/index.ts | 2 + packages/support/src/reflections/isKeySafe.ts | 13 ++++++ .../support/src/reflections/isKeyUnsafe.ts | 15 +++++++ .../reflections/isKey-Safe-Unsafe.test.js | 42 +++++++++++++++++++ 4 files changed, 72 insertions(+) create mode 100644 packages/support/src/reflections/isKeySafe.ts create mode 100644 packages/support/src/reflections/isKeyUnsafe.ts create mode 100644 tests/browser/packages/support/reflections/isKey-Safe-Unsafe.test.js diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index 9f6f204d..8523e799 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -10,5 +10,7 @@ export * from './hasPrototypeProperty'; export * from './isCallable'; export * from './isClassConstructor'; export * from './isConstructor'; +export * from './isKeySafe'; +export * from './isKeyUnsafe'; export * from './isSubclass'; export * from './isWeakKind'; \ No newline at end of file diff --git a/packages/support/src/reflections/isKeySafe.ts b/packages/support/src/reflections/isKeySafe.ts new file mode 100644 index 00000000..ce302d67 --- /dev/null +++ b/packages/support/src/reflections/isKeySafe.ts @@ -0,0 +1,13 @@ +import {isKeyUnsafe} from "@aedart/support/reflections/isKeyUnsafe"; + +/** + * Opposite of {@link isKeyUnsafe} + * + * @param {PropertyKey} key + * + * @returns {boolean} + */ +export function isKeySafe(key: PropertyKey): boolean +{ + return !isKeyUnsafe(key); +} \ No newline at end of file diff --git a/packages/support/src/reflections/isKeyUnsafe.ts b/packages/support/src/reflections/isKeyUnsafe.ts new file mode 100644 index 00000000..71d7c269 --- /dev/null +++ b/packages/support/src/reflections/isKeyUnsafe.ts @@ -0,0 +1,15 @@ +import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; + +/** + * Determine if property key is unsafe + * + * @see DANGEROUS_PROPERTIES + * + * @param {PropertyKey} key + * + * @returns {boolean} + */ +export function isKeyUnsafe(key: PropertyKey): boolean +{ + return DANGEROUS_PROPERTIES.includes(key); +} \ No newline at end of file diff --git a/tests/browser/packages/support/reflections/isKey-Safe-Unsafe.test.js b/tests/browser/packages/support/reflections/isKey-Safe-Unsafe.test.js new file mode 100644 index 00000000..d4d67998 --- /dev/null +++ b/tests/browser/packages/support/reflections/isKey-Safe-Unsafe.test.js @@ -0,0 +1,42 @@ +import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; +import { isKeySafe, isKeyUnsafe } from "@aedart/support/reflections"; + +describe('@aedart/support/objects', () => { + + const dataSet = [ + { value: 'name', safe: true, name: 'name' }, + { value: 'prototype', safe: true, name: 'prototype' }, + ]; + + for (const key of DANGEROUS_PROPERTIES) { + dataSet.push( + { + value: key, + safe: false, + name: typeof key == 'symbol' + ? key.description + : key + }, + ); + } + + describe('isKeySafe()', () => { + it('can determine if key is safe', () => { + for (const data of dataSet) { + expect(isKeySafe(data.value)) + .withContext(`${data.name} was expected to ${data.safe.toString()}`) + .toBe(data.safe); + } + }); + }); + + describe('isKeyUnsafe()', () => { + it('can determine if key is unsafe', () => { + for (const data of dataSet) { + expect(isKeyUnsafe(data.value)) + .withContext(`${data.name} was expected to ${data.safe.toString()}`) + .toBe(!data.safe); + } + }); + }); +}); \ No newline at end of file From 30d4052146c6f5b41fbbd7a97044c962fd9f1bb0 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Wed, 21 Feb 2024 20:56:52 +0100 Subject: [PATCH 205/424] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06b87763..23fd5365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `getParentOfClass()` and `getAllParentsOfClass()` in `@aedart/support/reflections`. * `getClassPropertyDescriptor()` and `getClassPropertyDescriptors()` in `@aedart/support/reflections`. * `isWeakKind()` in `@aedart/support/reflections`. +* `isKeySafe()` and `isKeyUnsafe()` in `@aedart/support/reflections`. * `getConstructorName()` and `getNameOrDesc()` in `@aedart/support/reflections`. * `isSubclass()` in `@aedart/support/reflections`. * `classOwnKeys()` in `@aedart/support/reflections`. From 91219e7fb549c612454d6ffdda67b77d9d230cd6 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Wed, 21 Feb 2024 20:58:02 +0100 Subject: [PATCH 206/424] Refactor, use isKeyUnsafe() util method --- packages/support/src/objects/merge/Merger.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/support/src/objects/merge/Merger.ts b/packages/support/src/objects/merge/Merger.ts index 8b499005..47dd696b 100644 --- a/packages/support/src/objects/merge/Merger.ts +++ b/packages/support/src/objects/merge/Merger.ts @@ -7,12 +7,12 @@ import type { SkipKeyCallback, ObjectsMerger } from "@aedart/contracts/support/objects"; -import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; import DefaultMergeOptions from "./DefaultMergeOptions"; import { MergeError } from "../exceptions"; import { getErrorMessage } from "@aedart/support/exceptions"; import { isCloneable } from "@aedart/support/objects"; import { descTag } from "@aedart/support/misc"; +import { isKeyUnsafe } from "@aedart/support/reflections"; /** * Objects Merger @@ -146,7 +146,7 @@ export default class Merger implements ObjectsMerger const keys: PropertyKey[] = Reflect.ownKeys(resolved); for (const key of keys){ // Skip key if needed ... - if (DANGEROUS_PROPERTIES.includes(key) || skipCallback(key, resolved, result)) { + if (isKeyUnsafe(key) || skipCallback(key, resolved, result)) { continue; } From 8f3e9f7e572630c2eb233e2af5c7d0e390260cea Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Wed, 21 Feb 2024 21:37:49 +0100 Subject: [PATCH 207/424] Add populate() util method --- packages/support/src/objects/index.ts | 1 + packages/support/src/objects/populate.ts | 53 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 packages/support/src/objects/populate.ts diff --git a/packages/support/src/objects/index.ts b/packages/support/src/objects/index.ts index 5de37eef..3e1cad26 100644 --- a/packages/support/src/objects/index.ts +++ b/packages/support/src/objects/index.ts @@ -9,6 +9,7 @@ export * from './isCloneable'; export * from './isset'; export * from './merge'; export * from './merger'; +export * from './populate'; export * from './set'; export * from './uniqueId'; diff --git a/packages/support/src/objects/populate.ts b/packages/support/src/objects/populate.ts new file mode 100644 index 00000000..a6ffedb6 --- /dev/null +++ b/packages/support/src/objects/populate.ts @@ -0,0 +1,53 @@ +import { isKeySafe } from "@aedart/support/reflections"; + +/** + * Populate target object with the properties from source object + * + * **Warning**: _This method performs a shallow copy of properties in source object!_ + * + * **Warning**: _`target` object is mutated!_ + * + * **Note**: _Properties that are [unsafe]{@link import('@aedart/support/reflections').isKeyUnsafe} are always disregarded!_ + * + * @param {object} target + * @param {object} source + * @param {PropertyKey | PropertyKey[]} [keys='*'] If wildcard (`*`) given, then all properties from the `source` are selected. + * @param {boolean} [safe=true] When `true`, properties must exist in target (_must be defined in target_), + * before they are shallow copied. + * + * @returns {object} The populated target + * + * @throws {TypeError} If a key does not exist in `target` (_when `safe = true`_). + * Or, if key does not exist in `source` (_regardless of `safe` flag_). + */ +export function populate(target: object, source: object, keys: PropertyKey|PropertyKey[] = '*', safe: boolean = true): object +{ + if (keys === '*') { + keys = Reflect.ownKeys(source); + } + + if (!Array.isArray(keys)) { + keys = [ keys ]; + } + + // Always remove dangerous keys + keys = keys.filter((key: PropertyKey) => isKeySafe(key)); + + // Populate... + for (const key of keys) { + // If "safe" is enabled, then only keys that are already defined in target are allowed. + if (safe && !Reflect.has(target, key)) { + throw new TypeError(`Key "${key.toString()}" does not exist in target object`); + } + + // However, fail if property does not exist in source, regardless of "safe" flag. + if (!Reflect.has(source, key)) { + throw new TypeError(`Key "${key.toString()}" does not exist in source object`); + } + + // @ts-expect-error At this point, all should be safe... + target[key] = source[key]; + } + + return target; +} \ No newline at end of file From 4c7a6b63c76ac1111038ddddfd988735f91c3df5 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Wed, 21 Feb 2024 21:37:56 +0100 Subject: [PATCH 208/424] Change release notes --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23fd5365..7b121784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `getConstructorName()` and `getNameOrDesc()` in `@aedart/support/reflections`. * `isSubclass()` in `@aedart/support/reflections`. * `classOwnKeys()` in `@aedart/support/reflections`. -* `merge()`, `merger()`, and `isCloneable()` in `@aedart/support/objects`. +* `merge()`, `merger()`, `populate()`, and `isCloneable()` in `@aedart/support/objects`. * `merge()`, `isTypedArray()`, `isArrayLike()`, `isSafeArrayLike()`, `isTypedArray()` and `isConcatSpreadable()` in `@aedart/support/arrays`. ## [0.8.0] - 2024-02-12 From ca347a0f21d8c08903ca5460cf5b4d856ae4849e Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Wed, 21 Feb 2024 21:41:36 +0100 Subject: [PATCH 209/424] Use only message if error is instance of Error We do not know what kind of "message" might be inside generic objects! --- packages/support/src/exceptions/getErrorMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/exceptions/getErrorMessage.ts b/packages/support/src/exceptions/getErrorMessage.ts index 7152fde7..8db2ecc0 100644 --- a/packages/support/src/exceptions/getErrorMessage.ts +++ b/packages/support/src/exceptions/getErrorMessage.ts @@ -8,7 +8,7 @@ */ export function getErrorMessage(error: unknown, defaultMessage: string = 'unknown reason'): string { - return (typeof error == 'object' && Reflect.has(error, 'message')) + return (typeof error == 'object' && error instanceof Error && Reflect.has(error, 'message')) ? error.message : defaultMessage; } \ No newline at end of file From 7f786b6a6bd8bb126f7dc5c0364a44e4810b2643 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Wed, 21 Feb 2024 21:41:55 +0100 Subject: [PATCH 210/424] Use getErrorMessage() --- packages/support/src/arrays/merge.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/support/src/arrays/merge.ts b/packages/support/src/arrays/merge.ts index b847d919..d0dea0ab 100644 --- a/packages/support/src/arrays/merge.ts +++ b/packages/support/src/arrays/merge.ts @@ -1,4 +1,5 @@ import { ArrayMergeError } from "./exceptions"; +import { getErrorMessage } from "@aedart/support/exceptions"; /** * Merge two or more arrays @@ -25,9 +26,7 @@ export function merge( return structuredClone([].concat(...sources)); } catch (e) { - const reason: string = (typeof e == 'object' && Reflect.has(e, 'message')) - ? e.message - : 'unknown reason'; + const reason = getErrorMessage(e); throw new ArrayMergeError('Unable to merge arrays: ' + reason, { cause: { From 318509ec35568dac36779c070175a5ac00a41b2f Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Wed, 21 Feb 2024 21:46:07 +0100 Subject: [PATCH 211/424] Use generic for target type --- packages/support/src/objects/populate.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/support/src/objects/populate.ts b/packages/support/src/objects/populate.ts index a6ffedb6..4c514452 100644 --- a/packages/support/src/objects/populate.ts +++ b/packages/support/src/objects/populate.ts @@ -9,6 +9,8 @@ import { isKeySafe } from "@aedart/support/reflections"; * * **Note**: _Properties that are [unsafe]{@link import('@aedart/support/reflections').isKeyUnsafe} are always disregarded!_ * + * @template T = object + * * @param {object} target * @param {object} source * @param {PropertyKey | PropertyKey[]} [keys='*'] If wildcard (`*`) given, then all properties from the `source` are selected. @@ -20,7 +22,12 @@ import { isKeySafe } from "@aedart/support/reflections"; * @throws {TypeError} If a key does not exist in `target` (_when `safe = true`_). * Or, if key does not exist in `source` (_regardless of `safe` flag_). */ -export function populate(target: object, source: object, keys: PropertyKey|PropertyKey[] = '*', safe: boolean = true): object +export function populate( + target: T, + source: object, + keys: PropertyKey|PropertyKey[] = '*', + safe: boolean = true +): T { if (keys === '*') { keys = Reflect.ownKeys(source); From 02b5893a9020fe319a1f81def5448a4b623b236c Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 09:46:52 +0100 Subject: [PATCH 212/424] Refactor, move merge specific type aliases into merge sub-directory --- .../src/support/objects/merge/MergeOptions.ts | 2 +- .../support/objects/merge/ObjectsMerger.ts | 3 +- .../src/support/objects/merge/index.ts | 2 + .../src/support/objects/merge/types.ts | 75 ++++++++++++++++++ .../contracts/src/support/objects/types.ts | 79 +------------------ 5 files changed, 82 insertions(+), 79 deletions(-) create mode 100644 packages/contracts/src/support/objects/merge/types.ts diff --git a/packages/contracts/src/support/objects/merge/MergeOptions.ts b/packages/contracts/src/support/objects/merge/MergeOptions.ts index ddddc93f..e6175228 100644 --- a/packages/contracts/src/support/objects/merge/MergeOptions.ts +++ b/packages/contracts/src/support/objects/merge/MergeOptions.ts @@ -1,7 +1,7 @@ import type { MergeCallback, SkipKeyCallback -} from "../types"; +} from "./types"; /** * Merge Options diff --git a/packages/contracts/src/support/objects/merge/ObjectsMerger.ts b/packages/contracts/src/support/objects/merge/ObjectsMerger.ts index dcd3a573..ffa013ef 100644 --- a/packages/contracts/src/support/objects/merge/ObjectsMerger.ts +++ b/packages/contracts/src/support/objects/merge/ObjectsMerger.ts @@ -1,4 +1,5 @@ -import type {MergeCallback, MergeOptions} from "@aedart/contracts/support/objects"; +import type { MergeCallback } from "./types"; +import type MergeOptions from "./MergeOptions"; /** * Objects Merger diff --git a/packages/contracts/src/support/objects/merge/index.ts b/packages/contracts/src/support/objects/merge/index.ts index d99081ee..66ac718c 100644 --- a/packages/contracts/src/support/objects/merge/index.ts +++ b/packages/contracts/src/support/objects/merge/index.ts @@ -15,3 +15,5 @@ export { type MergeSourceInfo, type ObjectsMerger } + +export * from './types'; \ No newline at end of file diff --git a/packages/contracts/src/support/objects/merge/types.ts b/packages/contracts/src/support/objects/merge/types.ts new file mode 100644 index 00000000..462fddb3 --- /dev/null +++ b/packages/contracts/src/support/objects/merge/types.ts @@ -0,0 +1,75 @@ +import type MergeOptions from './MergeOptions'; +import type MergeSourceInfo from './MergeSourceInfo'; + +/** + * Callback to perform the merging of nested objects. + * Invoking this callback results in the merge callback to + * be repeated, for the given source objects. + * + * @type {function} + */ +export type NextCallback = ( + + /** + * The nested objects to be merged + * + * @type {object[]} + */ + sources: object[], + + /** + * The merge options to be applied + * + * @type {Readonly} + */ + options: Readonly, + + /** + * The next recursion depth number + * + * @type {number} + */ + nextDepth: number +) => any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + +/** + * Merge callback function + * + * The callback is responsible for resolving the value to be merged into the resulting object. + * + * **Note**: _[Skipped keys]{@link MergeOptions.skip} are NOT provided to the callback._ + * + * **Note**: _The callback is responsible for respecting the given [options]{@link MergeOptions}, + * ([keys to be skipped]{@link MergeOptions.skip} and [depth]{@link MergeOptions.depth} excluded)_ + */ +export type MergeCallback = ( + + /** + * Source target information + * + * @type {MergeSourceInfo} + */ + target: MergeSourceInfo, + + /** + * Callback to invoke for merging nested objects + * + * @type {function} + */ + next: NextCallback, + + /** + * The merge options to be applied + */ + options: Readonly +) => any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + +/** + * A callback that determines if property key should be skipped + * + * If the callback returns `true`, then the given key will NOT be merged + * from the given source object. + * + * @see {MergeOptions} + */ +export type SkipKeyCallback = (key: PropertyKey, source: object, result: object) => boolean; \ No newline at end of file diff --git a/packages/contracts/src/support/objects/types.ts b/packages/contracts/src/support/objects/types.ts index 67845d4e..89f8f836 100644 --- a/packages/contracts/src/support/objects/types.ts +++ b/packages/contracts/src/support/objects/types.ts @@ -1,77 +1,2 @@ -import type { - MergeOptions, - MergeSourceInfo -} from "./merge"; - -/** - * Callback to perform the merging of nested objects. - * Invoking this callback results in the merge callback to - * be repeated, for the given source objects. - * - * @type {function} - */ -export type NextCallback = ( - - /** - * The nested objects to be merged - * - * @type {object[]} - */ - sources: object[], - - /** - * The merge options to be applied - * - * @type {Readonly} - */ - options: Readonly, - - /** - * The next recursion depth number - * - * @type {number} - */ - nextDepth: number -) => any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ - -/** - * Merge callback function - * - * The callback is responsible for resolving the value to be merged into the resulting object. - * - * **Note**: _[Skipped keys]{@link MergeOptions.skip} are NOT provided to the callback._ - * - * **Note**: _The callback is responsible for respecting the given [options]{@link MergeOptions}, - * ([keys to be skipped]{@link MergeOptions.skip} and [depth]{@link MergeOptions.depth} excluded)_ - */ -export type MergeCallback = ( - - /** - * Source target information - * - * @type {MergeSourceInfo} - */ - target: MergeSourceInfo, - - /** - * Callback to invoke for merging nested objects - * - * @type {function} - */ - next: NextCallback, - - /** - * The merge options to be applied - */ - options: Readonly -) => any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ - -/** - * A callback that determines if property key should be skipped - * - * If the callback returns `true`, then the given key will NOT be merged - * from the given source object. - * - * @see {MergeOptions} - */ -export type SkipKeyCallback = (key: PropertyKey, source: object, result: object) => boolean; \ No newline at end of file +// TODO: Replace this... +export type TMP = string; \ No newline at end of file From 04fa77904f147d48027e7c47a80244e547a7b959 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 09:58:50 +0100 Subject: [PATCH 213/424] Refactor populate, add support for callback to resolve source keys --- .../contracts/src/support/objects/types.ts | 11 +++++-- packages/support/src/objects/populate.ts | 29 ++++++++++++------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/contracts/src/support/objects/types.ts b/packages/contracts/src/support/objects/types.ts index 89f8f836..cf97eac1 100644 --- a/packages/contracts/src/support/objects/types.ts +++ b/packages/contracts/src/support/objects/types.ts @@ -1,2 +1,9 @@ -// TODO: Replace this... -export type TMP = string; \ No newline at end of file +/** + * Callback that returns properties to be selected from the source object. + * The target is the object intended to have source properties merged or assigned + * into. + */ +export type SourceKeysCallback< + SourceObj extends object = object, + TargetObj extends object = object +> = (source: SourceObj, target: TargetObj) => PropertyKey|PropertyKey[]; \ No newline at end of file diff --git a/packages/support/src/objects/populate.ts b/packages/support/src/objects/populate.ts index 4c514452..dca7b7e4 100644 --- a/packages/support/src/objects/populate.ts +++ b/packages/support/src/objects/populate.ts @@ -1,4 +1,5 @@ import { isKeySafe } from "@aedart/support/reflections"; +import type { SourceKeysCallback } from "@aedart/contracts/support/objects"; /** * Populate target object with the properties from source object @@ -9,11 +10,14 @@ import { isKeySafe } from "@aedart/support/reflections"; * * **Note**: _Properties that are [unsafe]{@link import('@aedart/support/reflections').isKeyUnsafe} are always disregarded!_ * - * @template T = object + * @template TargetObj extends object = object + * @template SourceObj extends object = object * * @param {object} target * @param {object} source - * @param {PropertyKey | PropertyKey[]} [keys='*'] If wildcard (`*`) given, then all properties from the `source` are selected. + * @param {PropertyKey | PropertyKey[] | SourceKeysCallback} [keys='*'] Keys to select and copy from `source` object. + * If wildcard (`*`) given, then all properties from the `source` + * are selected. * @param {boolean} [safe=true] When `true`, properties must exist in target (_must be defined in target_), * before they are shallow copied. * @@ -22,23 +26,28 @@ import { isKeySafe } from "@aedart/support/reflections"; * @throws {TypeError} If a key does not exist in `target` (_when `safe = true`_). * Or, if key does not exist in `source` (_regardless of `safe` flag_). */ -export function populate( - target: T, - source: object, - keys: PropertyKey|PropertyKey[] = '*', +export function populate< + TargetObj extends object = object, + SourceObj extends object = object +>( + target: TargetObj, + source: SourceObj, + keys: PropertyKey | PropertyKey[] | SourceKeysCallback = '*', safe: boolean = true -): T +): TargetObj { if (keys === '*') { keys = Reflect.ownKeys(source); + } else if (typeof keys == 'function') { + keys = (keys as SourceKeysCallback)(source, target); } if (!Array.isArray(keys)) { - keys = [ keys ]; + keys = [ keys as PropertyKey ]; } - // Always remove dangerous keys - keys = keys.filter((key: PropertyKey) => isKeySafe(key)); + // Always remove dangerous keys, regardless of "safe" flag. + keys = (keys as PropertyKey[]).filter((key: PropertyKey) => isKeySafe(key)); // Populate... for (const key of keys) { From 64f4c53342baac2dd01426a7a5eaadfaa44b1b95 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 10:46:20 +0100 Subject: [PATCH 214/424] Add tests for populate() util method --- .../packages/support/objects/populate.test.js | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 tests/browser/packages/support/objects/populate.test.js diff --git a/tests/browser/packages/support/objects/populate.test.js b/tests/browser/packages/support/objects/populate.test.js new file mode 100644 index 00000000..04cc47b4 --- /dev/null +++ b/tests/browser/packages/support/objects/populate.test.js @@ -0,0 +1,206 @@ +import { populate } from "@aedart/support/objects"; + +describe('@aedart/support/objects', () => { + describe('populate()', () => { + + it('populates all source properties into target', () => { + + class A { + name = null; + age = null; + + constructor(data) { + populate(this, data); + } + } + + const data = { + name: 'Ally', + age: 28 + }; + + // --------------------------------------------------------------------------- // + + const result = new A(data); + + expect(result.name) + .withContext('Property "name" not populated correctly') + .toBe(data.name); + + expect(result.age) + .withContext('Property "age" not populated correctly') + .toBe(data.age); + }); + + it('populates selected source properties into target', () => { + + class A { + name = null; + age = null; + + constructor(data) { + populate(this, data, 'name'); + } + } + + const data = { + name: 'Ally', + age: 28 + }; + + // --------------------------------------------------------------------------- // + + const result = new A(data); + + expect(result.name) + .withContext('Property "name" not populated correctly') + .toBe(data.name); + + expect(result.age) + .withContext('Property "age" not populated correctly') + .toBeNull(); + }); + + it('can select source keys via callback', () => { + + const a = { + name: null, + age: null, + }; + + const b = { + name: 'John Doe', age: 29 + } + + const c = { + name: 'Gwen', age: 42 + }; + + // --------------------------------------------------------------------------- // + + const callback = (source) => { + const keys = [ 'name' ]; + + if (Reflect.has(source, 'age') && source.age < 40) { + keys.push('age'); + } + + return keys; + } + + // Populate with b... + const result = populate(a, b, callback); + + // Populate again, but with c + populate(a, c, callback); + + expect(result.name) + .withContext('Property "name" (from c) not populated correctly') + .toBe(c.name); + + expect(result.age) + .withContext('Property "age" (from b) not populated correctly') + .toBe(b.age); + }); + + it('prevents prototype pollution', () => { + + const A = { + name: null, + age: null, + } + + const data = { + __proto__: { + admin: true + }, + name: 'Ally', + age: 28 + }; + + // --------------------------------------------------------------------------- // + + // NOTE: Reflect.ownKeys never returns '__proto__', so here we attempt to force + // populate() to inject it (which should be prevented) + const result = populate(A, data, [ + 'name', + 'age', + '__proto__' // Attempt to trick populate... + ]); + + // Debug + // console.log('__proto__', result, result.__proto__); + + const prototype = Reflect.getPrototypeOf(result); + + expect(Reflect.has(prototype, 'admin')) + .withContext('Prototype pollution NOT prevented') + .toBeFalse(); + + expect(result.name) + .withContext('Property "name" not populated correctly') + .toBe(data.name); + + expect(result.age) + .withContext('Property "age" not populated correctly') + .toBe(data.age); + }); + + it('fails if key does not exist in target', () => { + + class A { + name = null; + age = null; + + constructor(data) { + populate(this, data); + } + } + + const data = { + name: 'Sweeney', + age: 36, + title: 'Gardner' + }; + + // --------------------------------------------------------------------------- // + + const callback = () => { + return new A(data); + } + + expect(callback) + .toThrowError(TypeError); + }); + + it('can inject properties that target does not have, when "safe" mode disabled', () => { + + class A { + name = null; + age = null; + + constructor(data) { + populate(this, data, '*', false); + } + } + + const data = { + name: 'Sweeney', + age: 36, + title: 'Gardner' + }; + + // --------------------------------------------------------------------------- // + + const result = new A(data); + + expect(Reflect.has(result, 'title')) + .withContext('Property "title" not populated') + .toBeTrue(); + + expect(result.title) + .withContext('Property "title" not populated correctly') + .toBe(data.title); + }); + }); +}); \ No newline at end of file From fbb6ec5109bde5c3175a5af091362ba61b6c1b2c Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 10:55:34 +0100 Subject: [PATCH 215/424] Fix internal type --- packages/support/src/objects/isCloneable.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/support/src/objects/isCloneable.ts b/packages/support/src/objects/isCloneable.ts index aa03329a..1d01e749 100644 --- a/packages/support/src/objects/isCloneable.ts +++ b/packages/support/src/objects/isCloneable.ts @@ -1,3 +1,5 @@ +import type { Cloneable } from "@aedart/contracts/support/objects"; + /** * Determine if target object is cloneable. * @@ -13,5 +15,5 @@ export function isCloneable(target: object): boolean return typeof target == 'object' && target !== null && Reflect.has(target, 'clone') - && typeof target.clone == 'function'; + && typeof (target as Cloneable)['clone'] == 'function'; } \ No newline at end of file From 8dbfa0cac8de67c65bd5e5db33161f3df0254e3b Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 11:18:53 +0100 Subject: [PATCH 216/424] Add hasMethod() and hasAllMethods() util methods --- .../support/src/reflections/hasAllMethods.ts | 22 +++++++++++++++++++ packages/support/src/reflections/hasMethod.ts | 14 ++++++++++++ packages/support/src/reflections/index.ts | 2 ++ 3 files changed, 38 insertions(+) create mode 100644 packages/support/src/reflections/hasAllMethods.ts create mode 100644 packages/support/src/reflections/hasMethod.ts diff --git a/packages/support/src/reflections/hasAllMethods.ts b/packages/support/src/reflections/hasAllMethods.ts new file mode 100644 index 00000000..e6e386bf --- /dev/null +++ b/packages/support/src/reflections/hasAllMethods.ts @@ -0,0 +1,22 @@ +/** + * Determine if given target object contains all given methods + * + * @param {object} target + * @param {...PropertyKey} [methods] + * + * @return {boolean} + */ +export function hasAllMethods(target: object, ...methods: PropertyKey[]): boolean +{ + if (!target || typeof target != 'object' || Array.isArray(target) || methods.length === 0) { + return false; + } + + for (const method of methods) { + if (!Reflect.has(target, method) || typeof (target as Record)[method] != 'function') { + return false; + } + } + + return true; +} \ No newline at end of file diff --git a/packages/support/src/reflections/hasMethod.ts b/packages/support/src/reflections/hasMethod.ts new file mode 100644 index 00000000..2d08635b --- /dev/null +++ b/packages/support/src/reflections/hasMethod.ts @@ -0,0 +1,14 @@ +import { hasAllMethods } from "./hasAllMethods"; + +/** + * Determine if given target object contains method + * + * @param {object} target + * @param {PropertyKey} method + * + * @return {boolean} + */ +export function hasMethod(target: object, method: PropertyKey): boolean +{ + return hasAllMethods(target, method); +} \ No newline at end of file diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index 8523e799..92a0b858 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -6,6 +6,8 @@ export * from './getClassPropertyDescriptors'; export * from './getConstructorName'; export * from './getNameOrDesc'; export * from './getParentOfClass'; +export * from './hasAllMethods'; +export * from './hasMethod'; export * from './hasPrototypeProperty'; export * from './isCallable'; export * from './isClassConstructor'; From 9b2521e0bebffa78c26ff26ba4e24056bdbadc6f Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 11:20:27 +0100 Subject: [PATCH 217/424] Add tests for hasMethod() and hasAllMethods() util methods --- .../has-method-all-methods.test.js | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/browser/packages/support/reflections/has-method-all-methods.test.js diff --git a/tests/browser/packages/support/reflections/has-method-all-methods.test.js b/tests/browser/packages/support/reflections/has-method-all-methods.test.js new file mode 100644 index 00000000..a7769bcb --- /dev/null +++ b/tests/browser/packages/support/reflections/has-method-all-methods.test.js @@ -0,0 +1,105 @@ +import { hasAllMethods, hasMethod } from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('hasAllMethods()', () => { + + it('can determine if target has methods', () => { + + const data = [ + { + value: null, + methods: [ 'foo', 'bar' ], + expected: false, + name: 'NUll' + }, + { + value: [], + methods: [ 'foo', 'bar' ], + expected: false, + name: 'Array' + }, + { + value: {}, + methods: [ 'foo', 'bar' ], + expected: false, + name: 'Object (empty)' + }, + { + value: { + foo: () => true, + }, + methods: [ 'foo', 'bar' ], + expected: false, + name: 'Object (with some methods)' + }, + { + value: { + foo: () => true, + bar: () => true, + }, + methods: [ 'foo', 'bar' ], + expected: true, + name: 'Object (with all methods)' + }, + ]; + + for (const entry of data) { + expect(hasAllMethods(entry.value, ...entry.methods)) + .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) + .toBe(entry.expected); + } + + }); + }); + + describe('hasMethod()', () => { + + it('can determine if target has method', () => { + + const data = [ + { + value: null, + method: 'foo', + expected: false, + name: 'NUll' + }, + { + value: [], + method: 'foo', + expected: false, + name: 'Array' + }, + { + value: {}, + method: 'foo', + expected: false, + name: 'Object (empty)' + }, + { + value: { + foo: () => true, + }, + method: 'bar', + expected: false, + name: 'Object (with some methods)' + }, + { + value: { + foo: () => true, + bar: () => true, + }, + method: 'bar', + expected: true, + name: 'Object (with all methods)' + }, + ]; + + for (const entry of data) { + expect(hasMethod(entry.value, entry.method)) + .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) + .toBe(entry.expected); + } + + }); + }); +}); \ No newline at end of file From b553095425f77cabead60a14bd9d968dc9e1e080 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 11:20:41 +0100 Subject: [PATCH 218/424] Refactor isCloneable, use hasMethod() util method --- packages/support/src/objects/isCloneable.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/support/src/objects/isCloneable.ts b/packages/support/src/objects/isCloneable.ts index 1d01e749..7996e72c 100644 --- a/packages/support/src/objects/isCloneable.ts +++ b/packages/support/src/objects/isCloneable.ts @@ -1,4 +1,4 @@ -import type { Cloneable } from "@aedart/contracts/support/objects"; +import { hasMethod } from "@aedart/support/reflections"; /** * Determine if target object is cloneable. @@ -12,8 +12,5 @@ import type { Cloneable } from "@aedart/contracts/support/objects"; */ export function isCloneable(target: object): boolean { - return typeof target == 'object' - && target !== null - && Reflect.has(target, 'clone') - && typeof (target as Cloneable)['clone'] == 'function'; + return hasMethod(target, 'clone'); } \ No newline at end of file From c2ac7c46eb3bc94f155163398b84d384ca2a9aab Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 11:26:16 +0100 Subject: [PATCH 219/424] Add Populatable interface --- .../src/support/objects/Populatable.ts | 22 +++++++++++++++++++ .../contracts/src/support/objects/index.ts | 2 ++ 2 files changed, 24 insertions(+) create mode 100644 packages/contracts/src/support/objects/Populatable.ts diff --git a/packages/contracts/src/support/objects/Populatable.ts b/packages/contracts/src/support/objects/Populatable.ts new file mode 100644 index 00000000..60f3624a --- /dev/null +++ b/packages/contracts/src/support/objects/Populatable.ts @@ -0,0 +1,22 @@ +/** + * Populatable + * + * Able to be populated (hydrated) with data + */ +export default interface Populatable +{ + /** + * Populate this component with data + * + * **Note**: _When no `data` is provided, then nothing is populated_ + * + * @param {any} [data] E.g. key-value pair (object), array, or any other kind + * of data. + * + * @throws {TypeError} When unable to populate with given data. Or, if type of data is + * not supported. + */ + populate( + data?: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): this +} \ No newline at end of file diff --git a/packages/contracts/src/support/objects/index.ts b/packages/contracts/src/support/objects/index.ts index b3ace525..cc2e588e 100644 --- a/packages/contracts/src/support/objects/index.ts +++ b/packages/contracts/src/support/objects/index.ts @@ -18,8 +18,10 @@ export const SUPPORT_OBJECTS: unique symbol = Symbol('@aedart/contracts/support/ export const DANGEROUS_PROPERTIES: PropertyKey[] = [ '__proto__' ]; import Cloneable from "./Cloneable"; +import Populatable from "./Populatable"; export { type Cloneable, + type Populatable, } export * from './merge'; From 1e031f26fac6ee54c240dc7d1204e58767d1b19d Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 11:26:35 +0100 Subject: [PATCH 220/424] Add isPopulatable() util method --- packages/support/src/objects/index.ts | 1 + packages/support/src/objects/isPopulatable.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 packages/support/src/objects/isPopulatable.ts diff --git a/packages/support/src/objects/index.ts b/packages/support/src/objects/index.ts index 3e1cad26..a9067560 100644 --- a/packages/support/src/objects/index.ts +++ b/packages/support/src/objects/index.ts @@ -6,6 +6,7 @@ export * from './hasAll'; export * from './hasAny'; export * from './hasUniqueId'; export * from './isCloneable'; +export * from './isPopulatable'; export * from './isset'; export * from './merge'; export * from './merger'; diff --git a/packages/support/src/objects/isPopulatable.ts b/packages/support/src/objects/isPopulatable.ts new file mode 100644 index 00000000..81d8fa1b --- /dev/null +++ b/packages/support/src/objects/isPopulatable.ts @@ -0,0 +1,16 @@ +import { hasMethod } from "@aedart/support/reflections"; + +/** + * Determine if target is populatable + * + * **Note**: _Method assumes that target is populatable if it implements the + * [Populatable]{@link import('@aedart/constracts/support/objects').Populatable} interface._ + * + * @param {object} target + * + * @return {boolean} + */ +export function isPopulatable(target: object): boolean +{ + return hasMethod(target, 'populate'); +} \ No newline at end of file From 7e18aa48f09276cdf6b4a5cc6bacf091271763f4 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 11:26:50 +0100 Subject: [PATCH 221/424] Add tests for isPopulatable() --- .../support/objects/isPopulatable.test.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/browser/packages/support/objects/isPopulatable.test.js diff --git a/tests/browser/packages/support/objects/isPopulatable.test.js b/tests/browser/packages/support/objects/isPopulatable.test.js new file mode 100644 index 00000000..0b95080b --- /dev/null +++ b/tests/browser/packages/support/objects/isPopulatable.test.js @@ -0,0 +1,25 @@ +import { isPopulatable } from "@aedart/support/objects"; + +describe('@aedart/support/objects', () => { + describe('isPopulatable', () => { + + it('can determine if is populatable', () => { + + const dataSet = [ + { value: [], expected: false, name: 'Array' }, + { value: null, expected: false, name: 'Null' }, + { value: {}, expected: false, name: 'Object' }, + { value: { populate: false }, expected: false, name: 'Object with populate property' }, + + { value: { populate: () => true }, expected: true, name: 'Object with populate function' }, + ]; + + for (const data of dataSet) { + expect(isPopulatable(data.value)) + .withContext(`${data.name} was expected to ${data.expected.toString()}`) + .toBe(data.expected); + } + }); + + }); +}); \ No newline at end of file From b03644f5b567d6e023579a786831efc70d6d0f5f Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 11:26:57 +0100 Subject: [PATCH 222/424] Change release notes --- CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b121784..ee969d09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `@aedart/contracts/support/arrays` and `@aedart/support/arrays` submodules. * `ConcatSpreadable` (_extends TypeScript's `ArrayLike` interface_) interface in `@aedart/contracts/support/arrays`. * `Throwable` (_extends TypeScript's `Error` interface_) interface in `@aedart/contracts/support/exceptions`. -* `Cloneable` interface in `@aedart/contracts/support/objects`. +* `Cloneable` and `Populatable` interfaces in `@aedart/contracts/support/objects`. * `LogicalError` and `AbstractClassError` exceptions in `@aedart/support/exceptions`. * `getErrorMessage()` in `@aedart/support/exceptions`. * `FUNCTION_PROTOTYPE` and `TYPED_ARRAY_PROTOTYPE` constants in `@aedart/contracts/support/reflections`. @@ -25,9 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `isWeakKind()` in `@aedart/support/reflections`. * `isKeySafe()` and `isKeyUnsafe()` in `@aedart/support/reflections`. * `getConstructorName()` and `getNameOrDesc()` in `@aedart/support/reflections`. -* `isSubclass()` in `@aedart/support/reflections`. -* `classOwnKeys()` in `@aedart/support/reflections`. -* `merge()`, `merger()`, `populate()`, and `isCloneable()` in `@aedart/support/objects`. +* `isSubclass()`, `classOwnKeys()`, `hasMethod()` and `hasAllMethods()` in `@aedart/support/reflections`. +* `merge()`, `merger()`, `populate()`, `isCloneable()` and `isPopulatable()` in `@aedart/support/objects`. * `merge()`, `isTypedArray()`, `isArrayLike()`, `isSafeArrayLike()`, `isTypedArray()` and `isConcatSpreadable()` in `@aedart/support/arrays`. ## [0.8.0] - 2024-02-12 From 0da3bd214d8708d57579f20cef7d9ca1db4632b2 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 11:30:43 +0100 Subject: [PATCH 223/424] Refactor default merge options, use populate() util method --- .../src/objects/merge/DefaultMergeOptions.ts | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/support/src/objects/merge/DefaultMergeOptions.ts b/packages/support/src/objects/merge/DefaultMergeOptions.ts index 187748a7..f0cc7f06 100644 --- a/packages/support/src/objects/merge/DefaultMergeOptions.ts +++ b/packages/support/src/objects/merge/DefaultMergeOptions.ts @@ -7,6 +7,7 @@ import { DEFAULT_MAX_MERGE_DEPTH } from "@aedart/contracts/support/objects"; import { MergeError } from "../exceptions"; import { defaultMergeCallback } from "./defaultMergeCallback"; import { makeSkipCallback } from "./makeSkipCallback"; +import { populate } from "@aedart/support/objects"; /** * Default Merge Options @@ -152,24 +153,7 @@ export default class DefaultMergeOptions implements MergeOptions public constructor(options?: MergeCallback | MergeOptions) { // Merge provided options, if any given if (options && typeof options == 'object') { - const supported: PropertyKey[] = Reflect.ownKeys(this) - .filter((key: PropertyKey) => { - return !['__proto__', 'prototype', 'constructor', 'makeDefaultSkipCallback'].includes(key as string); - }); - - Reflect.ownKeys(options) - .forEach((key) => { - if (!supported.includes(key)) { - const k = (typeof key == 'symbol') - ? key.description - : key; - - throw new MergeError(`Unsupported merge option key: ${k}`); - } - - // @ts-expect-error We are safe to set option value - this[key] = options[key]; - }); + populate(this, options); } // Abort in case of invalid maximum depth - other options can also be asserted, but they are less important. From a23a40404963826abbcca6633413413f10cc1b3f Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 11:41:45 +0100 Subject: [PATCH 224/424] Improve JSDoc --- .../support/objects/merge/ObjectsMerger.ts | 85 ++++++++++++------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/packages/contracts/src/support/objects/merge/ObjectsMerger.ts b/packages/contracts/src/support/objects/merge/ObjectsMerger.ts index ffa013ef..51ee9977 100644 --- a/packages/contracts/src/support/objects/merge/ObjectsMerger.ts +++ b/packages/contracts/src/support/objects/merge/ObjectsMerger.ts @@ -24,10 +24,12 @@ export default interface ObjectsMerger * * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} * of all given sources._ + * + * @template SourceA extends object + * + * @param {SourceA} a * - * @param {object} a - * - * @returns {object} + * @returns {SourceA} * * @throws {MergeException} */ @@ -41,10 +43,13 @@ export default interface ObjectsMerger * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} * of all given sources._ * - * @param {object} a - * @param {object} b + * @template SourceA extends object + * @template SourceB extends object + * + * @param {SourceA} a + * @param {SourceB} b * - * @returns {object} + * @returns {SourceA & SourceB} * * @throws {MergeException} */ @@ -59,11 +64,15 @@ export default interface ObjectsMerger * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} * of all given sources._ * - * @param {object} a - * @param {object} b - * @param {object} c + * @template SourceA extends object + * @template SourceB extends object + * @template SourceC extends object + * + * @param {SourceA} a + * @param {SourceB} b + * @param {SourceC} c * - * @returns {object} + * @returns {SourceA & SourceB & SourceC} * * @throws {MergeException} */ @@ -79,12 +88,17 @@ export default interface ObjectsMerger * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} * of all given sources._ * - * @param {object} a - * @param {object} b - * @param {object} c - * @param {object} d + * @template SourceA extends object + * @template SourceB extends object + * @template SourceC extends object + * @template SourceD extends object + * + * @param {SourceA} a + * @param {SourceB} b + * @param {SourceC} c + * @param {SourceD} d * - * @returns {object} + * @returns {SourceA & SourceB & SourceC & SourceD} * * @throws {MergeException} */ @@ -101,13 +115,19 @@ export default interface ObjectsMerger * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} * of all given sources._ * - * @param {object} a - * @param {object} b - * @param {object} c - * @param {object} d - * @param {object} e + * @template SourceA extends object + * @template SourceB extends object + * @template SourceC extends object + * @template SourceD extends object + * @template SourceE extends object * - * @returns {object} + * @param {SourceA} a + * @param {SourceB} b + * @param {SourceC} c + * @param {SourceD} d + * @param {SourceE} e + * + * @returns {SourceA & SourceB & SourceC & SourceD & SourceE} * * @throws {MergeException} */ @@ -125,14 +145,21 @@ export default interface ObjectsMerger * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} * of all given sources._ * - * @param {object} a - * @param {object} b - * @param {object} c - * @param {object} d - * @param {object} e - * @param {object} f - * - * @returns {object} + * @template SourceA extends object + * @template SourceB extends object + * @template SourceC extends object + * @template SourceD extends object + * @template SourceE extends object + * @template SourceF extends object + * + * @param {SourceA} a + * @param {SourceB} b + * @param {SourceC} c + * @param {SourceD} d + * @param {SourceE} e + * @param {SourceF} f + * + * @returns {SourceA & SourceB & SourceC & SourceD & SourceE & SourceF} * * @throws {MergeException} */ From 1269393363bf2e282ebc95a86680cf1b393cab1b Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 12:51:44 +0100 Subject: [PATCH 225/424] Remove merger This will shortly be replaced by a different version of the merge() util method, which covers this use-case. --- packages/support/src/objects/merger.ts | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 packages/support/src/objects/merger.ts diff --git a/packages/support/src/objects/merger.ts b/packages/support/src/objects/merger.ts deleted file mode 100644 index 8a41f879..00000000 --- a/packages/support/src/objects/merger.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { - MergeOptions, - MergeCallback, - ObjectsMerger -} from "@aedart/contracts/support/objects"; -import Merger from "./merge/Merger"; - -/** - * Returns a new objects merger instance - * - * @param {MergeCallback | MergeOptions} [options] - * - * @return {ObjectsMerger} - */ -export function merger(options?: MergeCallback | MergeOptions): ObjectsMerger -{ - return new Merger(options); -} \ No newline at end of file From b6c7a3ddac9634cd6b67991d5fa8c93b26a67e2d Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 12:52:00 +0100 Subject: [PATCH 226/424] Fix Lodash references --- packages/support/src/objects/forget.ts | 2 +- packages/support/src/objects/get.ts | 2 +- packages/support/src/objects/has.ts | 2 +- packages/support/src/objects/set.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/support/src/objects/forget.ts b/packages/support/src/objects/forget.ts index 2a24808b..22bdb1ed 100644 --- a/packages/support/src/objects/forget.ts +++ b/packages/support/src/objects/forget.ts @@ -2,7 +2,7 @@ import { unset as _unset } from 'lodash-es'; /** * Remove value in object at given path - * (Alias for Lodash' {@link import('lodash').unset unset}) method + * (Alias for Lodash' [unset]{@link import('lodash').unset}) method * * @type {(object: any, path: import('@aedart/contracts/support').Key) => boolean} */ diff --git a/packages/support/src/objects/get.ts b/packages/support/src/objects/get.ts index 53ce99fa..d864fb1f 100644 --- a/packages/support/src/objects/get.ts +++ b/packages/support/src/objects/get.ts @@ -8,7 +8,7 @@ import {get as _get} from 'lodash-es'; /** * Get value from object that matches given path - * (Alias for Lodash' {@link import('lodash').get get}) method + * (Alias for Lodash' [get]{@link import('lodash').get}) method * * @type {{(object: TObject, path: ([TKey] | TKey)): TObject[TKey], (object: (TObject | null | undefined), path: ([TKey] | TKey)): (TObject[TKey] | undefined), (object: (TObject | null | undefined), path: ([TKey] | TKey), defaultValue: TDefault): (Exclude | TDefault), (object: TObject, path: [TKey1, TKey2]): TObject[TKey1][TKey2], (object: (TObject | null | undefined), path: [TKey1, TKey2]): (TObject[TKey1][TKey2] | undefined), (object: (TObject | null | undefined), path: [TKey1, TKey2], defaultValue: TDefault): (Exclude | TDefault), (object: TObject, path: [TKey1, TKey2, TKey3]): TObject[TKey1][TKey2][TKey3], (object: (TObject | null | undefined), path: [TKey1, TKey2, TKey3]): (TObject[TKey1][TKey2][TKey3] | undefined), (object: (TObject | null | undefined), path: [TKey1, TKey2, TKey3], defaultValue: TDefault): (Exclude | TDefault), (object: TObject, path: [TKey1, TKey2, TKey3, TKey4]): TObject[TKey1][TKey2][TKey3][TKey4], (object: (TObject | null | undefined), path: [TKey1, TKey2, TKey3, TKey4]): (TObject[TKey1][TKey2][TKey3][TKey4] | undefined), (object: (TObject | null | undefined), path: [TKey1, TKey2, TKey3, TKey4], defaultValue: TDefault): (Exclude | TDefault), (object: NumericDictionary, path: number): T, (object: (NumericDictionary | null | undefined), path: number): (T | undefined), (object: (NumericDictionary | null | undefined), path: number, defaultValue: TDefault): (T | TDefault), (object: (null | undefined), path: PropertyPath, defaultValue: TDefault): TDefault, (object: (null | undefined), path: PropertyPath): undefined, (data: TObject, path: TPath): string extends TPath ? any : GetFieldType, >(data: TObject, path: TPath, defaultValue: TDefault): (Exclude, null | undefined> | TDefault), (object: any, path: PropertyPath, defaultValue?: any): any}} */ diff --git a/packages/support/src/objects/has.ts b/packages/support/src/objects/has.ts index 9246727d..11d222da 100644 --- a/packages/support/src/objects/has.ts +++ b/packages/support/src/objects/has.ts @@ -3,7 +3,7 @@ import {hasIn as _hasIn} from 'lodash-es'; /** * Determine if path is a property of given object * - * (Alias for Lodash' {@link import('lodash').hasIn hasIn}) method + * (Alias for Lodash' [hasIn]{@link import('lodash').hasIn}) method * * @type {(object: T, path: import('@aedart/contracts/support').Key) => boolean} */ diff --git a/packages/support/src/objects/set.ts b/packages/support/src/objects/set.ts index 63bddb0f..566d3bda 100644 --- a/packages/support/src/objects/set.ts +++ b/packages/support/src/objects/set.ts @@ -6,7 +6,7 @@ import { set as _set } from 'lodash-es'; /** * Set value in object at given path - * (Alias for Lodash' {@link import('lodash').set set}) method + * (Alias for Lodash' [set]{@link import('lodash').set}) method * * @type {{(object: T, path: Key, value: any): T, (object: object, path: Key, value: any): TResult}} */ From 497cb884eff3fc05368dd5f61cfda887f84221f2 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 12:56:27 +0100 Subject: [PATCH 227/424] Change merge() arguments signature and return type Now, when calling merge() without arguments, the underlying Objects Merger instance is returned. This makes usage much more elegant. --- packages/support/src/objects/index.ts | 1 - packages/support/src/objects/merge.ts | 184 +++++++++++++++++- .../packages/support/objects/merge.test.js | 133 +++++++++---- 3 files changed, 266 insertions(+), 52 deletions(-) diff --git a/packages/support/src/objects/index.ts b/packages/support/src/objects/index.ts index a9067560..ffcc75d2 100644 --- a/packages/support/src/objects/index.ts +++ b/packages/support/src/objects/index.ts @@ -9,7 +9,6 @@ export * from './isCloneable'; export * from './isPopulatable'; export * from './isset'; export * from './merge'; -export * from './merger'; export * from './populate'; export * from './set'; export * from './uniqueId'; diff --git a/packages/support/src/objects/merge.ts b/packages/support/src/objects/merge.ts index 799e28fb..b7fa4b82 100644 --- a/packages/support/src/objects/merge.ts +++ b/packages/support/src/objects/merge.ts @@ -1,21 +1,185 @@ -import type { - MergeOptions, - MergeCallback, -} from "@aedart/contracts/support/objects"; -import { merger } from './merger' +import type { ObjectsMerger } from "@aedart/contracts/support/objects"; +import Merger from "./merge/Merger"; + +/** + * Returns a new Object Merger instance + * + * @return {ObjectsMerger} + */ +export function merge(): ObjectsMerger; /** * Returns a merger of given source objects * * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} * of all given sources._ - * - * @param {object[]} sources - * @param {MergeCallback | MergeOptions} [options] Merge callback or merge options. + * + * @template SourceA extends object + * + * @param {SourceA} a + * + * @returns {SourceA} + * + * @throws {MergeError} + */ +export function merge< + SourceA extends object +>(a: SourceA): SourceA; + +/** + * Returns a merger of given source objects + * + * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} + * of all given sources._ + * + * @template SourceA extends object + * @template SourceB extends object + * + * @param {SourceA} a + * @param {SourceB} b + * + * @returns {SourceA & SourceB} + * + * @throws {MergeError} + */ +export function merge< + SourceA extends object, + SourceB extends object, +>(a: SourceA, b: SourceB): SourceA & SourceB; + +/** + * Returns a merger of given source objects + * + * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} + * of all given sources._ + * + * @template SourceA extends object + * @template SourceB extends object + * @template SourceC extends object + * + * @param {SourceA} a + * @param {SourceB} b + * @param {SourceC} c + * + * @returns {SourceA & SourceB & SourceC} + * + * @throws {MergeError} + */ +export function merge< + SourceA extends object, + SourceB extends object, + SourceC extends object, +>(a: SourceA, b: SourceB, c: SourceC): SourceA & SourceB & SourceC; + +/** + * Returns a merger of given source objects + * + * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} + * of all given sources._ + * + * @template SourceA extends object + * @template SourceB extends object + * @template SourceC extends object + * @template SourceD extends object + * + * @param {SourceA} a + * @param {SourceB} b + * @param {SourceC} c + * @param {SourceD} d + * + * @returns {SourceA & SourceB & SourceC & SourceD} + * + * @throws {MergeError} + */ +export function merge< + SourceA extends object, + SourceB extends object, + SourceC extends object, + SourceD extends object, +>(a: SourceA, b: SourceB, c: SourceC, d: SourceD): SourceA & SourceB & SourceC & SourceD; + +/** + * Returns a merger of given source objects + * + * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} + * of all given sources._ + * + * @template SourceA extends object + * @template SourceB extends object + * @template SourceC extends object + * @template SourceD extends object + * @template SourceE extends object + * + * @param {SourceA} a + * @param {SourceB} b + * @param {SourceC} c + * @param {SourceD} d + * @param {SourceE} e + * + * @returns {SourceA & SourceB & SourceC & SourceD & SourceE} * * @throws {MergeError} */ -export function merge(sources: object[], options?: MergeCallback | MergeOptions) +export function merge< + SourceA extends object, + SourceB extends object, + SourceC extends object, + SourceD extends object, + SourceE extends object, +>(a: SourceA, b: SourceB, c: SourceC, d: SourceD, e: SourceE): SourceA & SourceB & SourceC & SourceD & SourceE; + +/** + * Returns a merger of given source objects + * + * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} + * of all given sources._ + * + * @template SourceA extends object + * @template SourceB extends object + * @template SourceC extends object + * @template SourceD extends object + * @template SourceE extends object + * @template SourceF extends object + * + * @param {SourceA} a + * @param {SourceB} b + * @param {SourceC} c + * @param {SourceD} d + * @param {SourceE} e + * @param {SourceF} f + * + * @returns {SourceA & SourceB & SourceC & SourceD & SourceE & SourceF} + * + * @throws {MergeError} + */ +export function merge< + SourceA extends object, + SourceB extends object, + SourceC extends object, + SourceD extends object, + SourceE extends object, + SourceF extends object, +>(a: SourceA, b: SourceB, c: SourceC, d: SourceD, e: SourceE, f: SourceF): SourceA & SourceB & SourceC & SourceD & SourceE & SourceF; + +/** + * Returns a merger of given source objects + * + * **Note**: _This method is responsible for returning [deep copy]{@link https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy} + * of all given sources._ + * + * @param {....object} [sources] + * + * @return {ObjectsMerger|object} + * + * @throws {MergeError} + */ +export function merge(...sources: object[]) { - return merger(options).of(...sources); + const merger: ObjectsMerger = new Merger(); + + if (sources.length == 0) { + return merger; + } + + return merger.of(...sources); } \ No newline at end of file diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js index e28bfd53..afc6faa4 100644 --- a/tests/browser/packages/support/objects/merge.test.js +++ b/tests/browser/packages/support/objects/merge.test.js @@ -2,13 +2,21 @@ import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; import { TYPED_ARRAY_PROTOTYPE } from "@aedart/contracts/support/reflections"; import { merge, - merger, + Merger, MergeError } from "@aedart/support/objects"; -describe('@aedart/support/objects', () => { +fdescribe('@aedart/support/objects', () => { describe('merge', () => { - + + it('returns object merger instance when no arguments given', () => { + + const result = merge(); + + expect(result) + .toBeInstanceOf(Merger); + }); + it('can merge primitive values', () => { const MY_SYMBOL_A = Symbol('a'); @@ -37,9 +45,8 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - // const result = merge([a, b]); - const result = merger().of(a, b); - + const result = merge(a, b); + // Debug //console.log('result', result); @@ -67,7 +74,7 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ]); + const result = merge(a, b); // Debug //console.log('result', result); @@ -86,8 +93,10 @@ describe('@aedart/support/objects', () => { }; // --------------------------------------------------------------------- // - - const result = merge([ a, b ], { overwriteWithUndefined: false }); + + const result = merge() + .using({ overwriteWithUndefined: false }) + .of(a, b); // Debug // console.log('result', result); @@ -107,14 +116,16 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ], (target, next, options) => { - const { key, value } = target; - if (key === 'b') { - return value + 1; - } - - return value; - }); + const result = merge() + .using((target, next, options) => { + const { key, value } = target; + if (key === 'b') { + return value + 1; + } + + return value; + }) + .of(a, b); // --------------------------------------------------------------------- // @@ -146,7 +157,7 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b, c ]); + const result = merge(a, b, c); // Debug // console.log('result', result); @@ -169,7 +180,9 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ], { skip: [ 'foo' ] }); + const result = merge() + .using({ skip: [ 'foo' ] }) + .of(a, b); // Debug // console.log('result', result); @@ -200,11 +213,13 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ], { - skip: (key, source) => { - return key === 'ab' && Reflect.has(source, key); - } - }); + const result = merge() + .using({ + skip: (key, source) => { + return key === 'ab' && Reflect.has(source, key); + } + }) + .of(a, b); // Debug // console.log('result', result); @@ -234,7 +249,7 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ]); + const result = merge(a, b); // Debug //console.log('result', result); @@ -264,7 +279,7 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ]); + const result = merge(a, b); // Debug // console.log('result', result); @@ -290,7 +305,11 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ], { mergeArrays: true }); + const result = merge() + .using({ + mergeArrays: true + }) + .of(a, b); // Debug // console.log('result', result); @@ -313,7 +332,7 @@ describe('@aedart/support/objects', () => { 'arr': [ function() {} ] }; - return merge([ a, b ] ); + return merge(a, b); } // --------------------------------------------------------------------- // @@ -352,7 +371,11 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ], { mergeArrays: true }); + const result = merge() + .using({ + mergeArrays: true + }) + .of(a, b); // Debug // console.log('result', result); @@ -403,7 +426,7 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ]); + const result = merge(a, b); // Debug // console.log('result', result); @@ -464,7 +487,11 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([a, b], { mergeArrays: true }); + const result = merge() + .using({ + mergeArrays: true + }) + .of(a, b); // Debug // console.log('result', result); @@ -500,7 +527,11 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ], { mergeArrays: true }); + const result = merge() + .using({ + mergeArrays: true + }) + .of(a, b); expect(Reflect.has(result, 'foo')) .withContext('Key with function value not merged') @@ -532,7 +563,11 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ], { mergeArrays: true }); + const result = merge() + .using({ + mergeArrays: true + }) + .of(a, b); // Debug // console.log('result', result) @@ -567,7 +602,11 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // const callback = () => { - return merge([ a, b ], { depth: -1 }); + return merge() + .using({ + depth: -1 + }) + .of(a, b); } // --------------------------------------------------------------------- // @@ -595,7 +634,11 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // const callback = () => { - return merge([ a, b ], { depth: 1 }); + return merge() + .using({ + depth: 1 + }) + .of(a, b); } // --------------------------------------------------------------------- // @@ -615,7 +658,11 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ], { depth: 0 }); + const result = merge() + .using({ + depth: 0 + }) + .of(a, b); // --------------------------------------------------------------------- // @@ -729,7 +776,7 @@ describe('@aedart/support/objects', () => { for (const entry of dataSet) { const target = {}; - const result = merge([ target, entry.source ]); + const result = merge(target, entry.source); expect(Reflect.has(result, 'value')) .withContext(`No value property in result for ${entry.name}`) @@ -764,7 +811,7 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ]); + const result = merge(a, b); // Debug // console.log('result', result); @@ -805,7 +852,7 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ]); + const result = merge(a, b); // Debug // console.log('result', result); @@ -839,7 +886,11 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // - const result = merge([ a, b ], { useCloneable: false }); + const result = merge() + .using({ + useCloneable: false + }) + .of(a, b); // Debug // console.log('result', result); @@ -869,7 +920,7 @@ describe('@aedart/support/objects', () => { // --------------------------------------------------------------------- // const callback = () => { - return merge([ a, b ]); + return merge(a, b); } expect(callback) From 7a5fac445f573cdf943be9e40a9592ca1a3992b3 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 12:56:46 +0100 Subject: [PATCH 228/424] Change usage of merge() --- .../support/src/reflections/getClassPropertyDescriptors.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/support/src/reflections/getClassPropertyDescriptors.ts b/packages/support/src/reflections/getClassPropertyDescriptors.ts index a0679bdb..3d3d7592 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptors.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptors.ts @@ -36,7 +36,7 @@ export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstru // Obtain property descriptors for all targets for (const t of targets) { const keys: PropertyKey[] = Reflect.ownKeys(t); - for (const key: PropertyKey of keys) { + for (const key of keys) { const descriptor: PropertyDescriptor | undefined = getClassPropertyDescriptor(t.constructor, key); // If for some reason we are unable to obtain a descriptor, then skip it. @@ -46,7 +46,9 @@ export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstru // Merge evt. existing descriptor object with the one obtained from target. if (Reflect.has(output, key)) { - output[key] = merge([ output[key], descriptor ], { overwriteWithUndefined: false }); + output[key] = merge() + .using({ overwriteWithUndefined: false }) + .of(output[key], descriptor); continue; } From c2cbddf29aa7c1b4ccd559f6002f3b40ec0173d5 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 13:04:38 +0100 Subject: [PATCH 229/424] Fix return type Not sure why "object" was set as Record key! --- .../src/reflections/getClassPropertyDescriptors.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/support/src/reflections/getClassPropertyDescriptors.ts b/packages/support/src/reflections/getClassPropertyDescriptors.ts index 3d3d7592..a56abbfd 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptors.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptors.ts @@ -14,12 +14,12 @@ import { merge } from "@aedart/support/objects"; * Descriptors are merged, such that the top-most class' descriptors * are returned. * - * @return {Record} Object with the property descriptors, or empty object of target has - * properties defined. + * @return {Record} Object with the property descriptors, or empty object of target has + * properties defined. * * @throws {TypeError} If target is not an object or has no prototype property */ -export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstructor, recursive: boolean = false): Record +export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstructor, recursive: boolean = false): Record { assertHasPrototypeProperty(target); @@ -31,7 +31,7 @@ export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstru targets = getAllParentsOfClass(target.prototype, true).reverse(); } - const output: Record = Object.create(null); + const output: Record = Object.create(null); // Obtain property descriptors for all targets for (const t of targets) { @@ -45,6 +45,7 @@ export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstru } // Merge evt. existing descriptor object with the one obtained from target. + if (Reflect.has(output, key)) { output[key] = merge() .using({ overwriteWithUndefined: false }) From 5a45167a49c4951cc1d5fbb86a419ae90079b4c6 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 13:04:51 +0100 Subject: [PATCH 230/424] Change release notes --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee969d09..c842e1a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,9 +26,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `isKeySafe()` and `isKeyUnsafe()` in `@aedart/support/reflections`. * `getConstructorName()` and `getNameOrDesc()` in `@aedart/support/reflections`. * `isSubclass()`, `classOwnKeys()`, `hasMethod()` and `hasAllMethods()` in `@aedart/support/reflections`. -* `merge()`, `merger()`, `populate()`, `isCloneable()` and `isPopulatable()` in `@aedart/support/objects`. +* `merge()`, `populate()`, `isCloneable()` and `isPopulatable()` in `@aedart/support/objects`. +* Objects `Merger` (_underlying component for the objects `merge()` util_) in `@aedart/support/objects`. * `merge()`, `isTypedArray()`, `isArrayLike()`, `isSafeArrayLike()`, `isTypedArray()` and `isConcatSpreadable()` in `@aedart/support/arrays`. +### Fixed + +* Lodash JSDoc references in `get()`, `set()`, `unset()` and `forget()`, in `@aedart/support/objects`. + ## [0.8.0] - 2024-02-12 ### Added From bcdb422ce5289ba96713f59fe8ad8eee392a09c4 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 13:06:46 +0100 Subject: [PATCH 231/424] Fix internal type --- packages/support/src/reflections/getNameOrDesc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/reflections/getNameOrDesc.ts b/packages/support/src/reflections/getNameOrDesc.ts index a7944004..a6a865ce 100644 --- a/packages/support/src/reflections/getNameOrDesc.ts +++ b/packages/support/src/reflections/getNameOrDesc.ts @@ -19,5 +19,5 @@ import { getConstructorName } from "./getConstructorName"; */ export function getNameOrDesc(target: ConstructorOrAbstractConstructor): string { - return getConstructorName(target, descTag(target)); + return getConstructorName(target, descTag(target)) as string; } \ No newline at end of file From ad590327e917856d290bc226935c7755c4e150c1 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 13:10:24 +0100 Subject: [PATCH 232/424] Improve internal types --- packages/support/src/reflections/hasPrototypeProperty.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/support/src/reflections/hasPrototypeProperty.ts b/packages/support/src/reflections/hasPrototypeProperty.ts index 29ceddea..666af7a2 100644 --- a/packages/support/src/reflections/hasPrototypeProperty.ts +++ b/packages/support/src/reflections/hasPrototypeProperty.ts @@ -11,5 +11,7 @@ */ export function hasPrototypeProperty(target: object): boolean { - return typeof target?.prototype == 'object' && target.prototype !== null; + return target + && typeof (target as Record)['prototype'] == 'object' + && (target as Record)['prototype'] !== null; } \ No newline at end of file From 67f450e56db1ab062b729ac476ce2a52463ae464 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 13:11:15 +0100 Subject: [PATCH 233/424] Fix internal type --- packages/support/src/reflections/isSubclass.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/reflections/isSubclass.ts b/packages/support/src/reflections/isSubclass.ts index d1199a41..09e92a31 100644 --- a/packages/support/src/reflections/isSubclass.ts +++ b/packages/support/src/reflections/isSubclass.ts @@ -19,5 +19,5 @@ export function isSubclass(target: object, superclass: ConstructorOrAbstractCons return false; } - return target.prototype instanceof superclass; + return (target as Record).prototype instanceof superclass; } \ No newline at end of file From 186e6c847c6dabc67e6af8b6772af7d950d66e35 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 13:12:23 +0100 Subject: [PATCH 234/424] Cleanup --- tests/browser/packages/support/objects/merge.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js index afc6faa4..012e3b30 100644 --- a/tests/browser/packages/support/objects/merge.test.js +++ b/tests/browser/packages/support/objects/merge.test.js @@ -6,7 +6,7 @@ import { MergeError } from "@aedart/support/objects"; -fdescribe('@aedart/support/objects', () => { +describe('@aedart/support/objects', () => { describe('merge', () => { it('returns object merger instance when no arguments given', () => { From 4a67abc072dcda7531a798868e0b5b3a108193a9 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 13:19:10 +0100 Subject: [PATCH 235/424] Fix returns true for null argument --- packages/support/src/reflections/hasPrototypeProperty.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/support/src/reflections/hasPrototypeProperty.ts b/packages/support/src/reflections/hasPrototypeProperty.ts index 666af7a2..29d1602d 100644 --- a/packages/support/src/reflections/hasPrototypeProperty.ts +++ b/packages/support/src/reflections/hasPrototypeProperty.ts @@ -1,3 +1,6 @@ +import {isset} from "@aedart/support/misc"; + + /** * Determine if target object has a prototype property defined * @@ -5,13 +8,15 @@ * _The method literally checks if a "prototype" property is defined in target, that it is not `null` or `undefined`, * and that its of the [type]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof} 'object'!_ * + * **Note**: _Method returns `false` if `null` given as argument!_ + * * @param {object} target * * @returns {boolean} */ export function hasPrototypeProperty(target: object): boolean { - return target + return isset(target) && typeof (target as Record)['prototype'] == 'object' && (target as Record)['prototype'] !== null; } \ No newline at end of file From 6e1eb24cc7dae7b4fd9dd44e1b44e30651dfb0c9 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 13:21:19 +0100 Subject: [PATCH 236/424] Fix internal types Es-lint complaint about "any" --- packages/support/src/reflections/hasAllMethods.ts | 2 +- packages/support/src/reflections/hasPrototypeProperty.ts | 4 ++-- packages/support/src/reflections/isSubclass.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/support/src/reflections/hasAllMethods.ts b/packages/support/src/reflections/hasAllMethods.ts index e6e386bf..9f896f33 100644 --- a/packages/support/src/reflections/hasAllMethods.ts +++ b/packages/support/src/reflections/hasAllMethods.ts @@ -13,7 +13,7 @@ export function hasAllMethods(target: object, ...methods: PropertyKey[]): boolea } for (const method of methods) { - if (!Reflect.has(target, method) || typeof (target as Record)[method] != 'function') { + if (!Reflect.has(target, method) || typeof (target as Record)[method] != 'function') { return false; } } diff --git a/packages/support/src/reflections/hasPrototypeProperty.ts b/packages/support/src/reflections/hasPrototypeProperty.ts index 29d1602d..7d553e82 100644 --- a/packages/support/src/reflections/hasPrototypeProperty.ts +++ b/packages/support/src/reflections/hasPrototypeProperty.ts @@ -17,6 +17,6 @@ import {isset} from "@aedart/support/misc"; export function hasPrototypeProperty(target: object): boolean { return isset(target) - && typeof (target as Record)['prototype'] == 'object' - && (target as Record)['prototype'] !== null; + && typeof (target as Record)['prototype'] == 'object' + && (target as Record)['prototype'] !== null; } \ No newline at end of file diff --git a/packages/support/src/reflections/isSubclass.ts b/packages/support/src/reflections/isSubclass.ts index 09e92a31..b8e60d88 100644 --- a/packages/support/src/reflections/isSubclass.ts +++ b/packages/support/src/reflections/isSubclass.ts @@ -19,5 +19,5 @@ export function isSubclass(target: object, superclass: ConstructorOrAbstractCons return false; } - return (target as Record).prototype instanceof superclass; + return (target as Record).prototype instanceof superclass; } \ No newline at end of file From 15bd005c8bd2b82a8d6a84c7117a68262fabb011 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 13:23:29 +0100 Subject: [PATCH 237/424] Use isset() to test if target argument is provided --- packages/support/src/reflections/hasAllMethods.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/support/src/reflections/hasAllMethods.ts b/packages/support/src/reflections/hasAllMethods.ts index 9f896f33..0f960ab8 100644 --- a/packages/support/src/reflections/hasAllMethods.ts +++ b/packages/support/src/reflections/hasAllMethods.ts @@ -1,3 +1,5 @@ +import { isset } from "@aedart/support/misc"; + /** * Determine if given target object contains all given methods * @@ -8,7 +10,7 @@ */ export function hasAllMethods(target: object, ...methods: PropertyKey[]): boolean { - if (!target || typeof target != 'object' || Array.isArray(target) || methods.length === 0) { + if (!isset(target) || typeof target != 'object' || Array.isArray(target) || methods.length === 0) { return false; } From bf57702c6e3b17bb0806231df4b51f44e2930d39 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 14:39:06 +0100 Subject: [PATCH 238/424] Add configureStackTrace() util method --- .../src/exceptions/configureStackTrace.ts | 29 ++++++++++++++++ packages/support/src/exceptions/index.ts | 1 + .../exceptions/configureStackTrace.test.js | 33 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 packages/support/src/exceptions/configureStackTrace.ts create mode 100644 tests/browser/packages/support/exceptions/configureStackTrace.test.js diff --git a/packages/support/src/exceptions/configureStackTrace.ts b/packages/support/src/exceptions/configureStackTrace.ts new file mode 100644 index 00000000..d849ad55 --- /dev/null +++ b/packages/support/src/exceptions/configureStackTrace.ts @@ -0,0 +1,29 @@ +import {isset} from "@aedart/support/misc"; + +/** + * Captures stack trace and sets stack trace for given error + * + * **Caution**: _Method will mutate given `error` by setting the `stack` property_ + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types + * + * @param {Error} error + * + * @return {string|undefined} Captured stack trace + */ +export function configureStackTrace(error: Error): string | undefined +{ + if (!isset(error)){ + return undefined; + } + + // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types + if (Reflect.has(Error, 'captureStackTrace') && typeof Error['captureStackTrace'] == 'function') { + (Error.captureStackTrace as (err: Error, errorConstructor: ErrorConstructor) => void)(error, Reflect.getPrototypeOf(error) as ErrorConstructor); + } else { + error.stack = (new Error()).stack; + } + + return error.stack; +} \ No newline at end of file diff --git a/packages/support/src/exceptions/index.ts b/packages/support/src/exceptions/index.ts index daee4a19..3b1d5da5 100644 --- a/packages/support/src/exceptions/index.ts +++ b/packages/support/src/exceptions/index.ts @@ -5,4 +5,5 @@ export { LogicalError } +export * from './configureStackTrace'; export * from './getErrorMessage'; \ No newline at end of file diff --git a/tests/browser/packages/support/exceptions/configureStackTrace.test.js b/tests/browser/packages/support/exceptions/configureStackTrace.test.js new file mode 100644 index 00000000..90b5e8bc --- /dev/null +++ b/tests/browser/packages/support/exceptions/configureStackTrace.test.js @@ -0,0 +1,33 @@ +import { configureStackTrace } from "@aedart/support/exceptions"; + +describe('@aedart/support/exceptions', () => { + + describe('configureStackTrace()', () => { + + it('can capture stack trace and set it in error instance', () => { + class MyCustomError extends Error + { + constructor(name, options) { + super(name, options); + + configureStackTrace(this); + + this.name = "MyCustomError"; + } + } + + // ---------------------------------------------------------------------------- // + + const error = new MyCustomError('Oh my...'); + + expect(Reflect.has(error, 'stack')) + .withContext('stack property not in error') + .toBeTrue(); + + expect(error.stack) + .withContext('stack property is undefined') + .not + .toBeUndefined(); + }); + }); +}); \ No newline at end of file From 0c1a25278fd427cb58f1eaf32cc28098c8fc8150 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 14:53:12 +0100 Subject: [PATCH 239/424] Add configureCustomError() util method --- .../src/exceptions/configureCustomError.ts | 35 ++++++++++++++++++ packages/support/src/exceptions/index.ts | 1 + .../exceptions/configureCustomError.test.js | 36 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 packages/support/src/exceptions/configureCustomError.ts create mode 100644 tests/browser/packages/support/exceptions/configureCustomError.test.js diff --git a/packages/support/src/exceptions/configureCustomError.ts b/packages/support/src/exceptions/configureCustomError.ts new file mode 100644 index 00000000..6947adc0 --- /dev/null +++ b/packages/support/src/exceptions/configureCustomError.ts @@ -0,0 +1,35 @@ +import { configureStackTrace } from "./configureStackTrace"; + +/** + * Configures the custom error + * + * **Note**: _Method configures error by setting its [stack trace]{@link configureStackTrace} + * and setting the error's common properties, e.g. name_ + * + * **Example**: + * ``` + * class MyCustomError extends Error + * { + * constructor(message, options) + * { + * super(message, options) + * + * configureCustomError(this); + * } + * } + * ``` + * + * @template T extends Error + * + * @param {Error} error + * + * @return {Error} + */ +export function configureCustomError(error: T): T +{ + configureStackTrace(error); + + error.name = Reflect.getPrototypeOf(error)?.constructor.name as string; + + return error; +} \ No newline at end of file diff --git a/packages/support/src/exceptions/index.ts b/packages/support/src/exceptions/index.ts index 3b1d5da5..2db4cc3b 100644 --- a/packages/support/src/exceptions/index.ts +++ b/packages/support/src/exceptions/index.ts @@ -5,5 +5,6 @@ export { LogicalError } +export * from './configureCustomError'; export * from './configureStackTrace'; export * from './getErrorMessage'; \ No newline at end of file diff --git a/tests/browser/packages/support/exceptions/configureCustomError.test.js b/tests/browser/packages/support/exceptions/configureCustomError.test.js new file mode 100644 index 00000000..c3f1cc42 --- /dev/null +++ b/tests/browser/packages/support/exceptions/configureCustomError.test.js @@ -0,0 +1,36 @@ +import { configureCustomError } from "@aedart/support/exceptions"; + +describe('@aedart/support/exceptions', () => { + + describe('configureCustomError()', () => { + + it('can configure custom error instance', () => { + class MyCustomError extends Error + { + constructor(name, options) { + super(name, options); + + configureCustomError(this); + } + } + + // ---------------------------------------------------------------------------- // + + const error = new MyCustomError('Oh my...'); + + expect(error.name) + .withContext('Custom Error name incorrect') + .toBe(MyCustomError.name) + + // Perhaps a bit redundant, ... + expect(Reflect.has(error, 'stack')) + .withContext('stack property not in error') + .toBeTrue(); + + expect(error.stack) + .withContext('stack property is undefined') + .not + .toBeUndefined(); + }); + }); +}); \ No newline at end of file From ca29abb03ac19edb46c468a1cd97cde138591b27 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 15:22:23 +0100 Subject: [PATCH 240/424] Change release notes --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c842e1a4..9d62f64d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `Throwable` (_extends TypeScript's `Error` interface_) interface in `@aedart/contracts/support/exceptions`. * `Cloneable` and `Populatable` interfaces in `@aedart/contracts/support/objects`. * `LogicalError` and `AbstractClassError` exceptions in `@aedart/support/exceptions`. -* `getErrorMessage()` in `@aedart/support/exceptions`. +* `getErrorMessage()`, `configureCustomError()` and `configureStackTrace()` in `@aedart/support/exceptions`. * `FUNCTION_PROTOTYPE` and `TYPED_ARRAY_PROTOTYPE` constants in `@aedart/contracts/support/reflections`. * `DANGEROUS_PROPERTIES` constant in `@aedart/contracts/support/objects`. * `hasPrototypeProperty()` and `assertHasPrototypeProperty()` in `@aedart/support/reflections`. From f74f340028879c013a62cbfd3eef40af282b6def Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 15:28:20 +0100 Subject: [PATCH 241/424] Refactor Errors, use configureCustomError() util --- .../support/src/arrays/exceptions/ArrayMergeError.ts | 9 ++------- packages/support/src/concerns/exceptions/BootError.ts | 9 ++------- .../support/src/concerns/exceptions/ConcernError.ts | 10 +++------- .../support/src/concerns/exceptions/InjectionError.ts | 10 +++------- .../src/concerns/exceptions/NotRegisteredError.ts | 9 ++------- packages/support/src/exceptions/AbstractClassError.ts | 11 +++-------- packages/support/src/exceptions/LogicalError.ts | 9 ++------- packages/support/src/objects/exceptions/MergeError.ts | 9 ++------- 8 files changed, 19 insertions(+), 57 deletions(-) diff --git a/packages/support/src/arrays/exceptions/ArrayMergeError.ts b/packages/support/src/arrays/exceptions/ArrayMergeError.ts index 86b75fa9..30bb962c 100644 --- a/packages/support/src/arrays/exceptions/ArrayMergeError.ts +++ b/packages/support/src/arrays/exceptions/ArrayMergeError.ts @@ -1,4 +1,5 @@ import type { Throwable } from "@aedart/contracts/support/exceptions"; +import { configureCustomError } from "@aedart/support/exceptions"; /** * Array Merge Error @@ -17,12 +18,6 @@ export default class ArrayMergeError extends Error implements Throwable { super(message, options); - if (Error.captureStackTrace) { - Error.captureStackTrace(this, ArrayMergeError); - } else { - this.stack = (new Error()).stack; - } - - this.name = "ArrayMergeError"; + configureCustomError(this); } } \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/BootError.ts b/packages/support/src/concerns/exceptions/BootError.ts index 68b7f6a8..982a50b5 100644 --- a/packages/support/src/concerns/exceptions/BootError.ts +++ b/packages/support/src/concerns/exceptions/BootError.ts @@ -1,6 +1,7 @@ import type { BootException, Concern } from "@aedart/contracts/support/concerns"; import type { Constructor} from "@aedart/contracts"; import ConcernError from "./ConcernError"; +import { configureCustomError } from "@aedart/support/exceptions"; /** * Concern Boot Error @@ -19,13 +20,7 @@ export default class BootError extends ConcernError implements BootException constructor(concern: Constructor, message: string, options?: ErrorOptions) { super(concern, message, options); - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, BootError); - } else { - this.stack = (new Error()).stack; - } - this.name = "BootError"; + configureCustomError(this); } } \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/ConcernError.ts b/packages/support/src/concerns/exceptions/ConcernError.ts index 8a05ae6f..cfd6cb8e 100644 --- a/packages/support/src/concerns/exceptions/ConcernError.ts +++ b/packages/support/src/concerns/exceptions/ConcernError.ts @@ -1,5 +1,6 @@ import type { ConcernException, Concern } from "@aedart/contracts/support/concerns"; import type { Constructor } from "@aedart/contracts"; +import { configureCustomError } from "@aedart/support/exceptions"; /** * Concern Error @@ -28,17 +29,12 @@ export default class ConcernError extends Error implements ConcernException { super(message, options || { cause: {} }); - if (Error.captureStackTrace) { - Error.captureStackTrace(this, ConcernError); - } else { - this.stack = (new Error()).stack; - } + configureCustomError(this); - this.name = "ConcernError"; this.#concern = concern; // Force set the concern in the cause (in case custom was provided) - this.cause.concern = concern; + (this.cause as Record).concern = concern; } /** diff --git a/packages/support/src/concerns/exceptions/InjectionError.ts b/packages/support/src/concerns/exceptions/InjectionError.ts index 9b145b9e..b894a0b6 100644 --- a/packages/support/src/concerns/exceptions/InjectionError.ts +++ b/packages/support/src/concerns/exceptions/InjectionError.ts @@ -1,6 +1,7 @@ import ConcernError from "./ConcernError"; import { Concern, InjectionException, MustUseConcerns } from "@aedart/contracts/support/concerns"; import type { Constructor, ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { configureCustomError } from "@aedart/support/exceptions"; /** * Injection Error @@ -34,17 +35,12 @@ export default class InjectionError extends ConcernError implements InjectionExc ) { super(concern, message, options); - if (Error.captureStackTrace) { - Error.captureStackTrace(this, ConcernError); - } else { - this.stack = (new Error()).stack; - } + configureCustomError(this); - this.name = "InjectionError"; this.#target = target; // Force set the target in the cause - this.cause.target = target; + (this.cause as Record).target = target; } /** diff --git a/packages/support/src/concerns/exceptions/NotRegisteredError.ts b/packages/support/src/concerns/exceptions/NotRegisteredError.ts index 5882d298..7238ad65 100644 --- a/packages/support/src/concerns/exceptions/NotRegisteredError.ts +++ b/packages/support/src/concerns/exceptions/NotRegisteredError.ts @@ -2,6 +2,7 @@ import type { Concern, NotRegisteredException } from "@aedart/contracts/support/ import type { Constructor } from "@aedart/contracts"; import ConcernError from './ConcernError'; import { getNameOrDesc } from "@aedart/support/reflections"; +import { configureCustomError } from "@aedart/support/exceptions"; /** * Concern Not Registered Error @@ -20,12 +21,6 @@ export default class NotRegisteredError extends ConcernError implements NotRegis { super(concern, `Concern ${getNameOrDesc(concern)} is not registered in concerns container`, options); - if (Error.captureStackTrace) { - Error.captureStackTrace(this, NotRegisteredError); - } else { - this.stack = (new Error()).stack; - } - - this.name = "NotRegisteredError"; + configureCustomError(this); } } \ No newline at end of file diff --git a/packages/support/src/exceptions/AbstractClassError.ts b/packages/support/src/exceptions/AbstractClassError.ts index 6a6340e5..dd24f697 100644 --- a/packages/support/src/exceptions/AbstractClassError.ts +++ b/packages/support/src/exceptions/AbstractClassError.ts @@ -1,6 +1,7 @@ import type { AbstractConstructor } from "@aedart/contracts"; import LogicalError from "./LogicalError"; import { getNameOrDesc } from "@aedart/support/reflections"; +import { configureCustomError } from "./configureCustomError"; /** * Abstract Class Error @@ -25,14 +26,8 @@ export default class AbstractClassError extends LogicalError constructor(target: AbstractConstructor, options?: ErrorOptions) { super(`Unable to create new instance of abstract class ${getNameOrDesc(target)}`, options || { cause: { target: target } }); - this.target = target; - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, AbstractClassError); - } else { - this.stack = (new Error()).stack; - } + configureCustomError(this); - this.name = "AbstractClassError"; + this.target = target; } } \ No newline at end of file diff --git a/packages/support/src/exceptions/LogicalError.ts b/packages/support/src/exceptions/LogicalError.ts index b3e90fed..ef25e145 100644 --- a/packages/support/src/exceptions/LogicalError.ts +++ b/packages/support/src/exceptions/LogicalError.ts @@ -1,4 +1,5 @@ import type { Throwable } from "@aedart/contracts/support/exceptions"; +import { configureCustomError } from "./configureCustomError"; /** * Logical Error @@ -18,12 +19,6 @@ export default class LogicalError extends Error implements Throwable constructor(message?: string, options?: ErrorOptions) { super(message, options); - if (Error.captureStackTrace) { - Error.captureStackTrace(this, LogicalError); - } else { - this.stack = (new Error()).stack; - } - - this.name = "LogicalError"; + configureCustomError(this); } } \ No newline at end of file diff --git a/packages/support/src/objects/exceptions/MergeError.ts b/packages/support/src/objects/exceptions/MergeError.ts index dbdb702a..413127bb 100644 --- a/packages/support/src/objects/exceptions/MergeError.ts +++ b/packages/support/src/objects/exceptions/MergeError.ts @@ -1,4 +1,5 @@ import type { MergeException } from "@aedart/contracts/support/objects"; +import { configureCustomError } from "@aedart/support/exceptions"; /** * Merge Error @@ -17,12 +18,6 @@ export default class MergeError extends Error implements MergeException { super(message, options); - if (Error.captureStackTrace) { - Error.captureStackTrace(this, MergeError); - } else { - this.stack = (new Error()).stack; - } - - this.name = "MergeError"; + configureCustomError(this); } } \ No newline at end of file From 17dd0c9c498e2b01212b3cf3734d76a42d8bd279 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 15:41:48 +0100 Subject: [PATCH 242/424] Mark as incomplete --- packages/support/src/concerns/isConcernClass.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/support/src/concerns/isConcernClass.ts b/packages/support/src/concerns/isConcernClass.ts index 85882e9a..fa399df3 100644 --- a/packages/support/src/concerns/isConcernClass.ts +++ b/packages/support/src/concerns/isConcernClass.ts @@ -10,6 +10,8 @@ import AbstractConcern from "./AbstractConcern"; const concernClassCache: WeakSet = new WeakSet(); /** + * TODO: INCOMPLETE + * * Determine if given target is a [Concern]{@link import('@aedart/contracts/support/concerns').Concern} class * * @param {object} target @@ -29,6 +31,8 @@ export function isConcernClass(target: object, force: boolean = false): boolean } try { + // TODO: THIS MUST CHANGE - its way too heavy to obtain class property descriptors here... Use hasAllMethods() on class prototype instead, or similar... + const descriptors = getClassPropertyDescriptors(target as ConstructorOrAbstractConstructor, true); if (Reflect.has(descriptors, 'concernOwner')) { From 3e340aca889a95a391fa6367980a83336c531396 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 15:50:38 +0100 Subject: [PATCH 243/424] Fix Aliases type --- packages/contracts/src/support/concerns/types.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/contracts/src/support/concerns/types.ts b/packages/contracts/src/support/concerns/types.ts index 05618e39..0f78abd1 100644 --- a/packages/contracts/src/support/concerns/types.ts +++ b/packages/contracts/src/support/concerns/types.ts @@ -7,13 +7,16 @@ import Concern from "./Concern"; export type Alias = PropertyKey; /** - * A record that defines one or more aliases for a {@link Concern}'s properties or methods. - * + * Key-value pair where the key corresponds to a property or method inside the {@link Concern} instance, + * and value is an "alias" for that property or method. + * * In this context an "alias" means a property or method that is added onto a target * class' prototype and acts as a proxy to the original property or method inside the - * concern class instance. + * concern class instance. */ -export type Aliases = Record; +export type Aliases = { + [K in keyof T]: Alias +}; /** * Array that holds a {@link Concern} class / Owner class pair. From e9393aaee528557782ab3d35e3a7d9393f92fb9c Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 16:08:46 +0100 Subject: [PATCH 244/424] Add PROPERTIES symbol for concern class constructor This should be a far better approach, rather than assuming all properties and methods are desired exposed. The AbstractConcern can offer a default implementation of this, but should allow developers to fairly easy overwrite this. --- .../contracts/src/support/concerns/index.ts | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index c4a9a5a1..324cb674 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -1,3 +1,5 @@ +import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; + /** * Support Concerns identifier * @@ -6,6 +8,8 @@ export const SUPPORT_CONCERNS: unique symbol = Symbol('@aedart/contracts/support/concerns'); /** + * @deprecated TODO: This MUST be redesigned, such that each Concern class can provide a list of what to expose + * * Symbol used by a {@link Concern} to define properties or methods that must be * "hidden" and not allowed to be aliased into a target class. * @@ -24,10 +28,33 @@ export const SUPPORT_CONCERNS: unique symbol = Symbol('@aedart/contracts/support * } * ``` * - * @type {Symbol} + * @type {symbol} */ export const HIDDEN: unique symbol = Symbol('hidden'); +/** + * Symbol used by a [concern class]{@link ConcernConstructor} to indicate what properties + * and methods can be aliased into a target class. + * + * **Note**: _Symbol MUST be used to as name for a "static" method in the desired Concern class._ + * + * **Example**: + * ```ts + * class MyConcern implements Concern + * { + * static [PROPERTIES](): PropertyKey[] + * { + * // ...not shown... + * } + * + * // ...remaining not shown... + * } + * ``` + * + * @type {symbol} + */ +export const PROPERTIES : unique symbol = Symbol('concern_properties'); + /** * Symbol used to define a list of the concern classes that a given target class * must use. @@ -55,6 +82,9 @@ export const CONCERNS: unique symbol = Symbol('concerns'); * @type {ReadonlyArray} */ export const ALWAYS_HIDDEN: ReadonlyArray = [ + + ...DANGEROUS_PROPERTIES, + // ----------------------------------------------------------------- // // Defined by Concern interface / Abstract Concern: // ----------------------------------------------------------------- // @@ -69,21 +99,21 @@ export const ALWAYS_HIDDEN: ReadonlyArray = [ // If the Concern defines any hidden properties or methods, // then such a method will not do any good in a target class. HIDDEN, + + // The static properties method (just in case) + PROPERTIES, // ----------------------------------------------------------------- // // Other properties and methods: // ----------------------------------------------------------------- // - // Object "prototype" property is too dangerous to tamper with, - // within the context of aliasing! - 'prototype', - // In case that a concern class uses other concerns, prevent them // from being aliased. CONCERNS, ]; import Concern from "./Concern"; +import ConcernConstructor from "./ConcernConstructor"; import Configuration from "./Configuration"; import Container from "./Container"; import MustUseConcerns from "./MustUseConcerns"; @@ -91,6 +121,7 @@ import Injector from "./Injector"; import Owner from "./Owner"; export { type Concern, + type ConcernConstructor, type Configuration, type Container, type MustUseConcerns, From 279e3a6e5eff263e1bd3653280076220bd146521 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 16:09:22 +0100 Subject: [PATCH 245/424] Add Concern Constructor interface --- .../support/concerns/ConcernConstructor.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 packages/contracts/src/support/concerns/ConcernConstructor.ts diff --git a/packages/contracts/src/support/concerns/ConcernConstructor.ts b/packages/contracts/src/support/concerns/ConcernConstructor.ts new file mode 100644 index 00000000..1c803dcc --- /dev/null +++ b/packages/contracts/src/support/concerns/ConcernConstructor.ts @@ -0,0 +1,35 @@ +import Concern from "./Concern"; +import { PROPERTIES } from "./index"; + +/** + * Concern Constructor + * + * @template T extends Concern + * + * @see Concern + */ +export default interface ConcernConstructor +{ + /** + * Creates a new concern instance + * + * @param {object} [owner] The owner class instance this concern is injected into. + * Defaults to `this` concern instance if none given. + * + * @throws {Error} When concern is unable to preform initialisation, e.g. caused + * by the owner or other circumstances. + */ + new (owner?: object): T; + + /** + * Returns list of property keys that this concern class offers + * + * **Note**: _Only properties and methods returned by this method can be aliased + * into a target class._ + * + * @static + * + * @return {PropertyKey[]} + */ + [PROPERTIES](): (keyof T)[]; +} \ No newline at end of file From bfa7effc3a85afed1ef91516426945f3e6e2cb1b Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 16:11:32 +0100 Subject: [PATCH 246/424] Fix spread arg. type --- packages/contracts/src/support/concerns/Container.ts | 2 +- packages/support/src/concerns/ConcernsContainer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index cf781824..ef9c2422 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -113,7 +113,7 @@ export default interface Container * @throws {ConcernException} * @throws {Error} */ - call(concern: Constructor, method: PropertyKey, ...args: unknown): unknown; + call(concern: Constructor, method: PropertyKey, ...args: unknown[]): unknown; /** * Set the value of given property in concern instance diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts index 84bc5fb3..4e64f49a 100644 --- a/packages/support/src/concerns/ConcernsContainer.ts +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -216,7 +216,7 @@ export default class ConcernsContainer implements Container * @throws {ConcernError} * @throws {Error} */ - public call(concern: Constructor, method: PropertyKey, ...args: unknown): unknown + public call(concern: Constructor, method: PropertyKey, ...args: unknown[]): unknown { return this.get(concern)[method](...args); } From 030721af65d7e1c805737ef2360703229d4ff319 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 22 Feb 2024 16:18:44 +0100 Subject: [PATCH 247/424] Use new Concern Constructor interface --- .../src/support/concerns/Injector.ts | 13 +- .../src/support/concerns/MustUseConcerns.ts | 4 +- .../contracts/src/support/concerns/types.ts | 9 +- .../support/src/concerns/ConcernsInjector.ts | 268 +++++++++--------- 4 files changed, 151 insertions(+), 143 deletions(-) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index 8b462c5d..4a65c9f2 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -1,5 +1,6 @@ import { Constructor } from "@aedart/contracts"; import Concern from "./Concern"; +import ConcernConstructor from "./ConcernConstructor"; import Configuration from "./Configuration"; import MustUseConcerns from "./MustUseConcerns"; @@ -36,13 +37,13 @@ export default interface Injector * @template T = object The target class that concern classes must be injected into * @template C = {@link Concern} * - * @param {Constructor | Configuration} concerns List of concern classes / injection configurations + * @param {ConcernConstructor | Configuration} concerns List of concern classes / injection configurations * * @returns {MustUseConcerns} The modified target class * * @throws {InjectionException} */ - inject(...concerns: (Constructor|Configuration)[]): MustUseConcerns; + inject(...concerns: (ConcernConstructor|Configuration)[]): MustUseConcerns; /** * Defines the concern classes that must be used by the target class. @@ -50,6 +51,7 @@ export default interface Injector * **Note**: _Method changes the target class, such that it implements and respects the * {@link MustUseConcerns} interface. The original target class' constructor remains the untouched!_ * + * @template C extends Concern * @template T = object * * @param {T} target The target class that must define the concern classes to be used @@ -60,7 +62,7 @@ export default interface Injector * @throws {InjectionException} If given concern classes conflict with target class' parent concern classes, * e.g. in case of duplicates. Or, if unable to modify target class. */ - defineConcerns(target: T, concerns: Constructor[]): MustUseConcerns; + defineConcerns(target: T, concerns: ConcernConstructor[]): MustUseConcerns; /** * Defines a concerns {@link Container} in target class' prototype. @@ -102,6 +104,7 @@ export default interface Injector * **Note**: _Method will do nothing, if a property or method already exists in the target class' prototype * chain, with the same name as given "alias"._ * + * @template C extends Concern * @template T = object * * @param {MustUseConcerns} target The target in which "alias" must be defined in @@ -115,10 +118,10 @@ export default interface Injector * @throws {InjectionException} If unable to define "alias" in target class, e.g. due to failure when obtaining * or defining [property descriptors]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#description}. */ - defineAlias( + defineAlias( target: MustUseConcerns, alias: PropertyKey, key: PropertyKey, - source: Constructor + source: ConcernConstructor ): boolean; } \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/MustUseConcerns.ts b/packages/contracts/src/support/concerns/MustUseConcerns.ts index 7c76c9ad..5d4c502a 100644 --- a/packages/contracts/src/support/concerns/MustUseConcerns.ts +++ b/packages/contracts/src/support/concerns/MustUseConcerns.ts @@ -1,5 +1,5 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; -import type { ConcernClasses } from "./index"; +import type { ConcernClasses, Concern } from "./index"; import { CONCERN_CLASSES } from "./index"; import Owner from "./Owner"; @@ -35,5 +35,5 @@ export default interface MustUseConcerns * * @return {ConcernClasses} */ - [CONCERN_CLASSES](): ConcernClasses; + [CONCERN_CLASSES](): ConcernClasses; } \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/types.ts b/packages/contracts/src/support/concerns/types.ts index 0f78abd1..32309490 100644 --- a/packages/contracts/src/support/concerns/types.ts +++ b/packages/contracts/src/support/concerns/types.ts @@ -1,5 +1,6 @@ -import { Constructor, ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import Concern from "./Concern"; +import ConcernConstructor from "./ConcernConstructor"; /** * An alias for a property or method in a {@link Concern} class @@ -21,8 +22,8 @@ export type Aliases = { /** * Array that holds a {@link Concern} class / Owner class pair. */ -export type ConcernOwnerClassPair = [ - Constructor, // Concern Class +export type ConcernOwnerClassPair = [ + ConcernConstructor, // Concern Class ConstructorOrAbstractConstructor // Owner class that must use the concern class ]; @@ -32,4 +33,4 @@ export type ConcernOwnerClassPair = [ * * @see ConcernOwnerClassPair */ -export type ConcernClasses = ConcernOwnerClassPair[]; \ No newline at end of file +export type ConcernClasses = ConcernOwnerClassPair[]; \ No newline at end of file diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index d7cc234c..40aee181 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -1,5 +1,6 @@ import { Concern, + ConcernConstructor, Injector, MustUseConcerns, Configuration @@ -46,137 +47,140 @@ export default class ConcernsInjector implements Injector { return this.#target; } - - /** - * Injects concern classes into the target class and return the modified target. - * - * **Note**: _Method performs injection in the following way:_ - * - * _**A**: Defines the concern classes in target class, via {@link defineConcerns}._ - * - * _**B**: Defines a concerns container in target class' prototype, via {@link defineContainer}._ - * - * _**C**: Defines "aliases" (proxy properties and methods) in target class' prototype, via {@link defineAliases}._ - * - * @template T = object The target class that concern classes must be injected into - * @template C = {@link Concern} - * - * @param {Constructor | Configuration} concerns List of concern classes / injection configurations - * - * @returns {MustUseConcerns} The modified target class - * - * @throws {InjectionException} - */ - public inject(...concerns: (Constructor|Configuration)[]): MustUseConcerns - { - // TODO: implement this method... - - // Resolve arguments, such that they are of type "concern injection configuration". - - // A) Define the concern classes in target class - - // B) Define a concerns container in target class' prototype - - // C) Define "aliases" (proxy properties and methods) in target class' prototype - - return this.target as MustUseConcerns; - } - /** - * Defines the concern classes that must be used by the target class. - * - * **Note**: _Method changes the target class, such that it implements and respects the - * {@link MustUseConcerns} interface. The original target class' constructor remains the untouched!_ - * - * @template T = object - * - * @param {T} target The target class that must define the concern classes to be used - * @param {Constructor[]} concerns List of concern classes - * - * @returns {MustUseConcerns} The modified target class - * - * @throws {InjectionException} If given concern classes conflict with target class' parent concern classes, - * e.g. in case of duplicates. Or, if unable to modify target class. - */ - public defineConcerns(target: T, concerns: Constructor[]): MustUseConcerns - { - // TODO: implement this method... - - return target as MustUseConcerns; - } - - /** - * Defines a concerns {@link Container} in target class' prototype. - * - * **Note**: _Method changes the target class, such that it implements and respects the - * [Owner]{@link import('@aedart/contracts/support/concerns').Owner} interface!_ - * - * @template T = object - * - * @param {MustUseConcerns} target The target in which a concerns container must be defined - * - * @returns {MustUseConcerns} The modified target class - * - * @throws {InjectionException} If unable to define concerns container in target class - */ - public defineContainer(target: MustUseConcerns): MustUseConcerns - { - // TODO: implement this method... - - return target; - } - - /** - * Defines "aliases" (proxy properties and methods) in target class' prototype, such that they - * point to the properties and methods available in the concern classes. - * - * **Note**: _Method defines each alias using the {@link defineAlias} method!_ - * - * @template T = object - * - * @param {MustUseConcerns} target The target in which "aliases" must be defined in - * @param {Configuration[]} configurations List of concern injection configurations - * - * @returns {MustUseConcerns} The modified target class - * - * @throws {InjectionException} If case of alias naming conflicts. Or, if unable to define aliases in target class. - */ - public defineAliases(target: MustUseConcerns, configurations: Configuration[]): MustUseConcerns - { - // TODO: implement this method... - - return target; - } - - /** - * Defines an "alias" (proxy property or method) in target class' prototype, to a property or method - * in given concern. - * - * **Note**: _Method will do nothing, if a property or method already exists in the target class' prototype - * chain, with the same name as given "alias"._ - * - * @template T = object - * - * @param {MustUseConcerns} target The target in which "alias" must be defined in - * @param {PropertyKey} alias Name of the "alias" in the target class (name of the proxy property or method) - * @param {PropertyKey} key Name of the property or method that the "alias" is for, in the concern class (`source`) - * @param {Constructor} source The concern class that holds the property or methods (`key`) - * - * @returns {boolean} `true` if "alias" was in target class. `false` if not, e.g. a property or method already - * exists in target class' prototype chain, with the same name as the alias. - * - * @throws {InjectionException} If unable to define "alias" in target class, e.g. due to failure when obtaining - * or defining [property descriptors]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#description}. - */ - public defineAlias( - target: MustUseConcerns, - alias: PropertyKey, - key: PropertyKey, - source: Constructor - ): boolean - { - // TODO: implement this method... - - return false; - } + // TODO: INCOMPLETE... + + // + // /** + // * Injects concern classes into the target class and return the modified target. + // * + // * **Note**: _Method performs injection in the following way:_ + // * + // * _**A**: Defines the concern classes in target class, via {@link defineConcerns}._ + // * + // * _**B**: Defines a concerns container in target class' prototype, via {@link defineContainer}._ + // * + // * _**C**: Defines "aliases" (proxy properties and methods) in target class' prototype, via {@link defineAliases}._ + // * + // * @template T = object The target class that concern classes must be injected into + // * @template C = {@link Concern} + // * + // * @param {Constructor | Configuration} concerns List of concern classes / injection configurations + // * + // * @returns {MustUseConcerns} The modified target class + // * + // * @throws {InjectionException} + // */ + // public inject(...concerns: (Constructor|Configuration)[]): MustUseConcerns + // { + // // TODO: implement this method... + // + // // Resolve arguments, such that they are of type "concern injection configuration". + // + // // A) Define the concern classes in target class + // + // // B) Define a concerns container in target class' prototype + // + // // C) Define "aliases" (proxy properties and methods) in target class' prototype + // + // return this.target as MustUseConcerns; + // } + // + // /** + // * Defines the concern classes that must be used by the target class. + // * + // * **Note**: _Method changes the target class, such that it implements and respects the + // * {@link MustUseConcerns} interface. The original target class' constructor remains the untouched!_ + // * + // * @template T = object + // * + // * @param {T} target The target class that must define the concern classes to be used + // * @param {Constructor[]} concerns List of concern classes + // * + // * @returns {MustUseConcerns} The modified target class + // * + // * @throws {InjectionException} If given concern classes conflict with target class' parent concern classes, + // * e.g. in case of duplicates. Or, if unable to modify target class. + // */ + // public defineConcerns(target: T, concerns: Constructor[]): MustUseConcerns + // { + // // TODO: implement this method... + // + // return target as MustUseConcerns; + // } + // + // /** + // * Defines a concerns {@link Container} in target class' prototype. + // * + // * **Note**: _Method changes the target class, such that it implements and respects the + // * [Owner]{@link import('@aedart/contracts/support/concerns').Owner} interface!_ + // * + // * @template T = object + // * + // * @param {MustUseConcerns} target The target in which a concerns container must be defined + // * + // * @returns {MustUseConcerns} The modified target class + // * + // * @throws {InjectionException} If unable to define concerns container in target class + // */ + // public defineContainer(target: MustUseConcerns): MustUseConcerns + // { + // // TODO: implement this method... + // + // return target; + // } + // + // /** + // * Defines "aliases" (proxy properties and methods) in target class' prototype, such that they + // * point to the properties and methods available in the concern classes. + // * + // * **Note**: _Method defines each alias using the {@link defineAlias} method!_ + // * + // * @template T = object + // * + // * @param {MustUseConcerns} target The target in which "aliases" must be defined in + // * @param {Configuration[]} configurations List of concern injection configurations + // * + // * @returns {MustUseConcerns} The modified target class + // * + // * @throws {InjectionException} If case of alias naming conflicts. Or, if unable to define aliases in target class. + // */ + // public defineAliases(target: MustUseConcerns, configurations: Configuration[]): MustUseConcerns + // { + // // TODO: implement this method... + // + // return target; + // } + // + // /** + // * Defines an "alias" (proxy property or method) in target class' prototype, to a property or method + // * in given concern. + // * + // * **Note**: _Method will do nothing, if a property or method already exists in the target class' prototype + // * chain, with the same name as given "alias"._ + // * + // * @template T = object + // * + // * @param {MustUseConcerns} target The target in which "alias" must be defined in + // * @param {PropertyKey} alias Name of the "alias" in the target class (name of the proxy property or method) + // * @param {PropertyKey} key Name of the property or method that the "alias" is for, in the concern class (`source`) + // * @param {Constructor} source The concern class that holds the property or methods (`key`) + // * + // * @returns {boolean} `true` if "alias" was in target class. `false` if not, e.g. a property or method already + // * exists in target class' prototype chain, with the same name as the alias. + // * + // * @throws {InjectionException} If unable to define "alias" in target class, e.g. due to failure when obtaining + // * or defining [property descriptors]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#description}. + // */ + // public defineAlias( + // target: MustUseConcerns, + // alias: PropertyKey, + // key: PropertyKey, + // source: Constructor + // ): boolean + // { + // // TODO: implement this method... + // + // return false; + // } } \ No newline at end of file From 708ffb5ebebf653732349306d96452b4f579c91c Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 22 Feb 2024 19:59:53 +0100 Subject: [PATCH 248/424] Add test of late static binding Needed to see if subclass' constructor would be returned, despite static method declared in superclass. --- .../packages/xyz/laste-static-binding.test.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/browser/packages/xyz/laste-static-binding.test.js diff --git a/tests/browser/packages/xyz/laste-static-binding.test.js b/tests/browser/packages/xyz/laste-static-binding.test.js new file mode 100644 index 00000000..754bb64a --- /dev/null +++ b/tests/browser/packages/xyz/laste-static-binding.test.js @@ -0,0 +1,25 @@ +xdescribe('@aedart/xyz', () => { + describe('late static binding', () => { + + it('can obtain constructor', () => { + class A { + + static foo() { + return this; // "this" SHOULD be late static, when invoked via B + } + } + + class B extends A {} + + // ---------------------------------------------------------------------- // + + const constructor = B.foo(); + + expect(constructor) + .withContext('Incorrect constructor') + .toBe(B) + + }); + + }); +}); \ No newline at end of file From 3ebe60b5ed563ef4dc971c40fc5ffa05043a0653 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 22 Feb 2024 20:08:24 +0100 Subject: [PATCH 249/424] Deprecate default implementation of HIDDEN --- packages/support/src/concerns/AbstractConcern.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index 23a4bef4..50f9458c 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -54,6 +54,8 @@ export default abstract class AbstractConcern implements Concern } /** + * @deprecated TODO: This must be removed again... To be replaced by [PROPERTIES]... + * * Returns a list of properties and methods that MUST NOT be aliased into the target class. * * **Warning**: _Regardless of what properties and methods this method may return, From 666788cdf074d52e09b268124b111b2de2b5ffcc Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 22 Feb 2024 20:10:33 +0100 Subject: [PATCH 250/424] Rename PROPERTIES symbol to PROVIDES Makes a bit more sense now. Also, simplified the return type of the [PROVIDES]() method. --- .../contracts/src/support/concerns/ConcernConstructor.ts | 6 +++--- packages/contracts/src/support/concerns/index.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/contracts/src/support/concerns/ConcernConstructor.ts b/packages/contracts/src/support/concerns/ConcernConstructor.ts index 1c803dcc..b002d420 100644 --- a/packages/contracts/src/support/concerns/ConcernConstructor.ts +++ b/packages/contracts/src/support/concerns/ConcernConstructor.ts @@ -1,5 +1,5 @@ import Concern from "./Concern"; -import { PROPERTIES } from "./index"; +import { PROVIDES } from "./index"; /** * Concern Constructor @@ -22,7 +22,7 @@ export default interface ConcernConstructor new (owner?: object): T; /** - * Returns list of property keys that this concern class offers + * Returns list of property keys that this concern class offers. * * **Note**: _Only properties and methods returned by this method can be aliased * into a target class._ @@ -31,5 +31,5 @@ export default interface ConcernConstructor * * @return {PropertyKey[]} */ - [PROPERTIES](): (keyof T)[]; + [PROVIDES](): PropertyKey[]; } \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 324cb674..92a46358 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -53,7 +53,7 @@ export const HIDDEN: unique symbol = Symbol('hidden'); * * @type {symbol} */ -export const PROPERTIES : unique symbol = Symbol('concern_properties'); +export const PROVIDES : unique symbol = Symbol('concern_properties'); /** * Symbol used to define a list of the concern classes that a given target class @@ -101,7 +101,7 @@ export const ALWAYS_HIDDEN: ReadonlyArray = [ HIDDEN, // The static properties method (just in case) - PROPERTIES, + PROVIDES, // ----------------------------------------------------------------- // // Other properties and methods: From 9e938f27b5a9ae674c2528f9b1a05f12541c2ace Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 22 Feb 2024 20:33:50 +0100 Subject: [PATCH 251/424] Fix call Reflect.hasOwn() on non-object Also added missing unit tests for classOwnKeys() util function. --- .../support/src/reflections/classOwnKeys.ts | 2 +- .../support/reflections/classOwnKeys.test.js | 83 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 tests/browser/packages/support/reflections/classOwnKeys.test.js diff --git a/packages/support/src/reflections/classOwnKeys.ts b/packages/support/src/reflections/classOwnKeys.ts index 4c0333e4..e117df3d 100644 --- a/packages/support/src/reflections/classOwnKeys.ts +++ b/packages/support/src/reflections/classOwnKeys.ts @@ -22,7 +22,7 @@ export function classOwnKeys(target: ConstructorOrAbstractConstructor, recursive } // Obtain target's parent classes... - const parents = getAllParentsOfClass(target.prototype, true); + const parents = getAllParentsOfClass(target, true).reverse(); const ownKeys: Set = new Set(); for (const parent of parents) { diff --git a/tests/browser/packages/support/reflections/classOwnKeys.test.js b/tests/browser/packages/support/reflections/classOwnKeys.test.js new file mode 100644 index 00000000..dbec3cb1 --- /dev/null +++ b/tests/browser/packages/support/reflections/classOwnKeys.test.js @@ -0,0 +1,83 @@ +import { classOwnKeys } from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('classOwnKeys()', () => { + + it('can return all class property keys', () => { + + class A { + foo() {} + + get bar() {} + } + + // ----------------------------------------------------------------------- // + + const result = classOwnKeys(A); + + // Debug + // console.log('result', result); + + expect(result) + .withContext('Incorrect property keys returned') + .toEqual([ 'constructor', 'foo', 'bar' ]); + }); + + it('can return class property keys recursively', () => { + + class A { + foo() {} + } + + class B extends A { + get bar() {} + } + + class C extends B { + zar() {} + } + + // ----------------------------------------------------------------------- // + + const result = classOwnKeys(C, true); + + // Debug + // console.log('result', result); + + expect(result) + .withContext('Incorrect property keys returned') + .toEqual([ 'constructor', 'foo', 'bar', 'zar' ]); + }); + + it('can return class property keys recursively (via class static method)', () => { + + class A { + a() {} + } + + class B extends A { + get b() {} + } + + class C extends B { + c() {} + + static keys() { + return classOwnKeys(this, true); + } + } + + // ----------------------------------------------------------------------- // + + const result = C.keys(); + + // Debug + // console.log('result', result); + + expect(result) + .withContext('Incorrect property keys returned') + .toEqual([ 'constructor', 'a', 'b', 'c' ]); + }); + + }); +}); \ No newline at end of file From f29ed7dfc75bab0289cdcc66dc17c6b62b0cc8b2 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 22 Feb 2024 21:04:14 +0100 Subject: [PATCH 252/424] Fix generic type for concern constructor Hmm... this might change again, but will have to do for now. --- packages/contracts/src/support/concerns/ConcernConstructor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/ConcernConstructor.ts b/packages/contracts/src/support/concerns/ConcernConstructor.ts index b002d420..f336c64f 100644 --- a/packages/contracts/src/support/concerns/ConcernConstructor.ts +++ b/packages/contracts/src/support/concerns/ConcernConstructor.ts @@ -8,7 +8,7 @@ import { PROVIDES } from "./index"; * * @see Concern */ -export default interface ConcernConstructor +export default interface ConcernConstructor { /** * Creates a new concern instance From 81f84dfac4b459cb7964518909004c86e875541f Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 22 Feb 2024 21:07:43 +0100 Subject: [PATCH 253/424] Add default implementation for [PROVIDES]() static method Also, added a way to cache the keys. This should increase the performance a bit, whenever the same concern class is used across multiple components. --- .../support/src/concerns/AbstractConcern.ts | 97 ++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index 50f9458c..f257583a 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -1,16 +1,39 @@ -import type { Concern } from "@aedart/contracts/support/concerns"; -import { HIDDEN, ALWAYS_HIDDEN } from "@aedart/contracts/support/concerns"; +import type { + Concern, + ConcernConstructor +} from "@aedart/contracts/support/concerns"; +import { + HIDDEN, + PROVIDES, + ALWAYS_HIDDEN +} from "@aedart/contracts/support/concerns"; import { AbstractClassError } from "@aedart/support/exceptions"; +import { classOwnKeys } from "@aedart/support/reflections"; +import {Constructor} from "@aedart/contracts"; /** * Abstract Concern * * @see {Concern} + * @see {ConcernConstructor} + * * @implements {Concern} + * * @abstract */ export default abstract class AbstractConcern implements Concern { + /** + * In-memory cache of resolved keys (properties and methods), which + * are offered by concern(s) and can be aliased. + * + * @type {WeakMap, PropertyKey[]>} + * + * @protected + * @static + */ + protected static resolvedConcernKeys: WeakMap, PropertyKey[]> = new WeakMap(); + /** * The owner class instance this concern is injected into, * or `this` concern instance. @@ -68,4 +91,74 @@ export default abstract class AbstractConcern implements Concern { return ALWAYS_HIDDEN; } + + /** + * Returns list of property keys that this concern class offers. + * + * **Note**: _Only properties and methods returned by this method can be aliased + * into a target class._ + * + * @static + * + * @return {PropertyKey[]} + */ + public static [PROVIDES](): PropertyKey[] + { + // Feel free to overwrite this static method in your concern class and specify + // the properties and methods that your concern offers (those that can be aliased). + + return this.rememberConcernKeys(this, () => { + return this.removeAlwaysHiddenKeys( + classOwnKeys(this, true) + ); + }); + } + + /** + * Removes keys that should remain hidden + * + * @see ALWAYS_HIDDEN + * + * @param {PropertyKey[]} keys + * + * @returns {PropertyKey[]} + * + * @protected + * @static + */ + protected static removeAlwaysHiddenKeys(keys: PropertyKey[]): PropertyKey[] + { + return keys.filter((key: PropertyKey) => { + return !ALWAYS_HIDDEN.includes(key); + }); + } + + /** + * Remember the resolved keys (properties and methods) for given target concern class + * + * @param {ThisType} concern + * @param {() => PropertyKey[]} callback + * @param {boolean} [force=false] + * + * @returns {PropertyKey[]} + * + * @protected + * @static + */ + protected static rememberConcernKeys( + concern: ThisType, + callback: () => PropertyKey[], + force: boolean = false + ): PropertyKey[] + { + if (!force && this.resolvedConcernKeys.has(concern)) { + return this.resolvedConcernKeys.get(concern) as PropertyKey[]; + } + + const keys: PropertyKey[] = callback(); + + this.resolvedConcernKeys.set(concern, keys); + + return keys; + } } \ No newline at end of file From 8c3c3e8cd1c733c2990cc02c0fd9592c8cb7cc56 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 22 Feb 2024 21:10:41 +0100 Subject: [PATCH 254/424] Add tests for default provides method --- .../support/concerns/AbstractConcern.test.js | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/tests/browser/packages/support/concerns/AbstractConcern.test.js b/tests/browser/packages/support/concerns/AbstractConcern.test.js index 1adeeee1..73d4b3bf 100644 --- a/tests/browser/packages/support/concerns/AbstractConcern.test.js +++ b/tests/browser/packages/support/concerns/AbstractConcern.test.js @@ -1,5 +1,5 @@ import { AbstractConcern } from "@aedart/support/concerns"; -import { HIDDEN } from "@aedart/contracts/support/concerns"; +import { HIDDEN, PROVIDES } from "@aedart/contracts/support/concerns"; import { AbstractClassError } from "@aedart/support/exceptions"; describe('@aedart/support/concerns', () => { @@ -39,8 +39,40 @@ describe('@aedart/support/concerns', () => { expect(result) .toBe(concern); }); - - it('returns default list of hidden properties and methods', () => { + + it('can obtain provided properties and methods', () => { + + class MyConcern extends AbstractConcern { + foo() {} + + get bar() {} + } + + class MyOtherConcern extends MyConcern { + sayHi() {} + } + + // const concern = new MyConcern(); + + // --------------------------------------------------------------------------------------- // + + const resultA = MyOtherConcern[PROVIDES](); + const resultB = MyConcern[PROVIDES](); // First concern class + + // Debug + console.log('result', resultA, resultB); + + expect(resultA) + .withContext('Incorrect properties for a') + .toEqual([ 'foo', 'bar', 'sayHi' ]); + + expect(resultB) + .withContext('Incorrect properties for b') + .toEqual([ 'foo', 'bar' ]); + }); + + // TODO: To be removed + xit('returns default list of hidden properties and methods', () => { class MyConcern extends AbstractConcern {} @@ -54,7 +86,8 @@ describe('@aedart/support/concerns', () => { .toEqual(0) }); - it('can overwrite default hidden', () => { + // TODO: To be removed + xit('can overwrite default hidden', () => { const newHidden = [ 'a', 'b', 'c' ]; class MyConcern extends AbstractConcern { From 3e0518e0b45a4cdd24cc37ba8e32780cdcbc4730 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 22 Feb 2024 21:11:16 +0100 Subject: [PATCH 255/424] Mark static methods from abstract concern as always hidden --- packages/contracts/src/support/concerns/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 92a46358..d9666cdb 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -100,8 +100,11 @@ export const ALWAYS_HIDDEN: ReadonlyArray = [ // then such a method will not do any good in a target class. HIDDEN, - // The static properties method (just in case) + // The static properties and methods (just in case...) PROVIDES, + 'resolvedConcernKeys', + 'removeAlwaysHiddenKeys', + 'rememberConcernKeys', // ----------------------------------------------------------------- // // Other properties and methods: From 445e614b8896edd2b5634e9bbc3d5759c1d0c284 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 22 Feb 2024 21:12:13 +0100 Subject: [PATCH 256/424] Fix style --- packages/support/src/concerns/AbstractConcern.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index f257583a..b684b036 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -9,7 +9,6 @@ import { } from "@aedart/contracts/support/concerns"; import { AbstractClassError } from "@aedart/support/exceptions"; import { classOwnKeys } from "@aedart/support/reflections"; -import {Constructor} from "@aedart/contracts"; /** * Abstract Concern From 1c01554b0ded0d7bfefcb9ccb453b7625b3a2b6b Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 22 Feb 2024 21:12:37 +0100 Subject: [PATCH 257/424] Fix style --- packages/contracts/src/support/concerns/Injector.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index 4a65c9f2..cdb2076c 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -1,4 +1,3 @@ -import { Constructor } from "@aedart/contracts"; import Concern from "./Concern"; import ConcernConstructor from "./ConcernConstructor"; import Configuration from "./Configuration"; From e0d93d2755a8ef83a798df23371ab16d983d8258 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Thu, 22 Feb 2024 21:13:29 +0100 Subject: [PATCH 258/424] Mark as incomplete --- packages/support/src/concerns/ConcernsInjector.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 40aee181..9eaa7ce3 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -8,6 +8,8 @@ import { import type { Constructor } from "@aedart/contracts"; /** + * TODO: Incomplete + * * Concerns Injector * * @see Injector From 5ffccce8980d8e1a86d8b87aaa77437301282654 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 09:05:41 +0100 Subject: [PATCH 259/424] Fix incorrect version of jasmine-core used by karma --- CHANGELOG.md | 1 + package-lock.json | 6 ------ package.json | 5 +++++ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d62f64d..1cecbf06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed * Lodash JSDoc references in `get()`, `set()`, `unset()` and `forget()`, in `@aedart/support/objects`. +* `jasmine-core` `v4.x` was used by `karma-jasmine`, which caused thrown exceptions that contained `ErrorOptions` to not being rendered correctly in the CLI. ## [0.8.0] - 2024-02-12 diff --git a/package-lock.json b/package-lock.json index 24e59e4d..a398895c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11413,12 +11413,6 @@ "karma": "^6.0.0" } }, - "node_modules/karma-jasmine/node_modules/jasmine-core": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz", - "integrity": "sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==", - "dev": true - }, "node_modules/karma-spec-reporter": { "version": "0.0.36", "resolved": "https://registry.npmjs.org/karma-spec-reporter/-/karma-spec-reporter-0.0.36.tgz", diff --git a/package.json b/package.json index 44ca5dd9..97b16e80 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,11 @@ "webpack": "^5.90.0", "webpack-cli": "^5.1.4" }, + "overrides": { + "karma-jasmine": { + "jasmine-core": "^5.1.2" + } + }, "scripts": { "bootstrap": "lerna bootstrap", "clean": "lerna clean && sh clean.sh", From 8d2081ef8ea50a3fe170c5c53575c96ae1092d68 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 09:05:49 +0100 Subject: [PATCH 260/424] Change release notes --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cecbf06..0e9938e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed * Lodash JSDoc references in `get()`, `set()`, `unset()` and `forget()`, in `@aedart/support/objects`. -* `jasmine-core` `v4.x` was used by `karma-jasmine`, which caused thrown exceptions that contained `ErrorOptions` to not being rendered correctly in the CLI. +* `jasmine-core` `v4.x` was used by `karma-jasmine`, which caused thrown exceptions that contained `ErrorOptions` to not being rendered correctly in the CLI. [_See GitHub issue for details_](https://github.com/jasmine/jasmine/issues/2028). ## [0.8.0] - 2024-02-12 From 0f87aff571bcf6103483231247605ed2e3d01a15 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 09:06:54 +0100 Subject: [PATCH 261/424] Add disabled test for throwing exception with error options Keeping this for future references. See https://github.com/jasmine/jasmine/issues/2028 --- .../packages/xyz/throws-exception-with-cause.test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/browser/packages/xyz/throws-exception-with-cause.test.js diff --git a/tests/browser/packages/xyz/throws-exception-with-cause.test.js b/tests/browser/packages/xyz/throws-exception-with-cause.test.js new file mode 100644 index 00000000..f085fe44 --- /dev/null +++ b/tests/browser/packages/xyz/throws-exception-with-cause.test.js @@ -0,0 +1,12 @@ +/** + * @see https://github.com/jasmine/jasmine/issues/2028 + */ +xdescribe('@aedart/xyz', () => { + it('fails...', () => { + + throw new Error('Oh my...', { + cause: { foo: 'bar' } + }); + + }); +}); \ No newline at end of file From 522ddb98028cad3acbfffce2cbf616ff2703fc56 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 09:08:33 +0100 Subject: [PATCH 262/424] Deprecate ALWAY_HIDDEN This MUST be moved into support/concerns, because it has become way too implementation specific. --- packages/contracts/src/support/concerns/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index d9666cdb..5a62b74a 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -76,6 +76,8 @@ export const CONCERN_CLASSES: unique symbol = Symbol('concern_classes'); export const CONCERNS: unique symbol = Symbol('concerns'); /** + * @deprecated TODO: Move this into support/concerns. It is way too implementation specific to belong here. + * * List of properties and methods that must always remain "hidden" and * **NEVER** be aliased into a target class' prototype. * From 5ff87be9f729d8bc124cdd6cbc6c60b087fcb16a Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 09:18:14 +0100 Subject: [PATCH 263/424] Simplify Abstract Concern, move caching and filtering to Injector The Injector should be responsible for caching concerns' provided properties and methods. The same applies for filtering out unsafe keys, etc. --- .../support/src/concerns/AbstractConcern.ts | 66 +--------------- .../support/src/concerns/ConcernsInjector.ts | 75 ++++++++++++++++++- .../support/concerns/AbstractConcern.test.js | 9 ++- 3 files changed, 80 insertions(+), 70 deletions(-) diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index b684b036..d310bb7a 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -1,6 +1,5 @@ import type { Concern, - ConcernConstructor } from "@aedart/contracts/support/concerns"; import { HIDDEN, @@ -22,17 +21,6 @@ import { classOwnKeys } from "@aedart/support/reflections"; */ export default abstract class AbstractConcern implements Concern { - /** - * In-memory cache of resolved keys (properties and methods), which - * are offered by concern(s) and can be aliased. - * - * @type {WeakMap, PropertyKey[]>} - * - * @protected - * @static - */ - protected static resolvedConcernKeys: WeakMap, PropertyKey[]> = new WeakMap(); - /** * The owner class instance this concern is injected into, * or `this` concern instance. @@ -106,58 +94,6 @@ export default abstract class AbstractConcern implements Concern // Feel free to overwrite this static method in your concern class and specify // the properties and methods that your concern offers (those that can be aliased). - return this.rememberConcernKeys(this, () => { - return this.removeAlwaysHiddenKeys( - classOwnKeys(this, true) - ); - }); - } - - /** - * Removes keys that should remain hidden - * - * @see ALWAYS_HIDDEN - * - * @param {PropertyKey[]} keys - * - * @returns {PropertyKey[]} - * - * @protected - * @static - */ - protected static removeAlwaysHiddenKeys(keys: PropertyKey[]): PropertyKey[] - { - return keys.filter((key: PropertyKey) => { - return !ALWAYS_HIDDEN.includes(key); - }); - } - - /** - * Remember the resolved keys (properties and methods) for given target concern class - * - * @param {ThisType} concern - * @param {() => PropertyKey[]} callback - * @param {boolean} [force=false] - * - * @returns {PropertyKey[]} - * - * @protected - * @static - */ - protected static rememberConcernKeys( - concern: ThisType, - callback: () => PropertyKey[], - force: boolean = false - ): PropertyKey[] - { - if (!force && this.resolvedConcernKeys.has(concern)) { - return this.resolvedConcernKeys.get(concern) as PropertyKey[]; - } - - const keys: PropertyKey[] = callback(); - - this.resolvedConcernKeys.set(concern, keys); - - return keys; + return classOwnKeys(this, true); } } \ No newline at end of file diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 9eaa7ce3..098aa5d5 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -1,10 +1,11 @@ -import { +import type { Concern, ConcernConstructor, Injector, MustUseConcerns, - Configuration + Configuration, } from "@aedart/contracts/support/concerns"; +import { ALWAYS_HIDDEN } from "@aedart/contracts/support/concerns"; import type { Constructor } from "@aedart/contracts"; /** @@ -185,4 +186,74 @@ export default class ConcernsInjector implements Injector // // return false; // } + + + // ------------------------------------------------------------------------- // + // TODO: Was previously part of AbstractConcern, but the logic should belong here + // TODO: instead... + + /** + * TODO: Adapt this... + * + * In-memory cache of resolved keys (properties and methods), which + * are offered by concern(s) and can be aliased. + * + * @type {WeakMap, PropertyKey[]>} + * + * @protected + * @static + */ + protected static resolvedConcernKeys: WeakMap, PropertyKey[]> = new WeakMap(); + + /** + * TODO: Adapt this... + * + * Removes keys that should remain hidden + * + * @see ALWAYS_HIDDEN + * + * @param {PropertyKey[]} keys + * + * @returns {PropertyKey[]} + * + * @protected + * @static + */ + protected static removeAlwaysHiddenKeys(keys: PropertyKey[]): PropertyKey[] + { + return keys.filter((key: PropertyKey) => { + return !ALWAYS_HIDDEN.includes(key); + }); + } + + /** + * TODO: Adapt this... + * + * Remember the resolved keys (properties and methods) for given target concern class + * + * @param {ThisType} concern + * @param {() => PropertyKey[]} callback + * @param {boolean} [force=false] + * + * @returns {PropertyKey[]} + * + * @protected + * @static + */ + protected static rememberConcernKeys( + concern: ThisType, + callback: () => PropertyKey[], + force: boolean = false + ): PropertyKey[] + { + if (!force && this.resolvedConcernKeys.has(concern)) { + return this.resolvedConcernKeys.get(concern) as PropertyKey[]; + } + + const keys: PropertyKey[] = callback(); + + this.resolvedConcernKeys.set(concern, keys); + + return keys; + } } \ No newline at end of file diff --git a/tests/browser/packages/support/concerns/AbstractConcern.test.js b/tests/browser/packages/support/concerns/AbstractConcern.test.js index 73d4b3bf..d194d280 100644 --- a/tests/browser/packages/support/concerns/AbstractConcern.test.js +++ b/tests/browser/packages/support/concerns/AbstractConcern.test.js @@ -60,15 +60,18 @@ describe('@aedart/support/concerns', () => { const resultB = MyConcern[PROVIDES](); // First concern class // Debug - console.log('result', resultA, resultB); + // console.log('result', resultA, resultB); + // Note: Abstract Concern is NOT responsible for filtering out unsafe (ALWAYS_HIDDEN) + // properties and methods! + expect(resultA) .withContext('Incorrect properties for a') - .toEqual([ 'foo', 'bar', 'sayHi' ]); + .toEqual([ 'constructor', 'concernOwner', 'foo', 'bar', 'sayHi' ]); expect(resultB) .withContext('Incorrect properties for b') - .toEqual([ 'foo', 'bar' ]); + .toEqual([ 'constructor', 'concernOwner', 'foo', 'bar' ]); }); // TODO: To be removed From 4a51f232a078c01efb3bac11128fb4b0c279d1fe Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 09:20:56 +0100 Subject: [PATCH 264/424] Remove the static HIDDEN() method Has now been replaced by the new [PROVIDES]() static method. --- .../support/src/concerns/AbstractConcern.ts | 26 +----------- .../support/concerns/AbstractConcern.test.js | 42 +------------------ 2 files changed, 3 insertions(+), 65 deletions(-) diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index d310bb7a..607d9a46 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -1,11 +1,5 @@ -import type { - Concern, -} from "@aedart/contracts/support/concerns"; -import { - HIDDEN, - PROVIDES, - ALWAYS_HIDDEN -} from "@aedart/contracts/support/concerns"; +import type { Concern } from "@aedart/contracts/support/concerns"; +import { PROVIDES } from "@aedart/contracts/support/concerns"; import { AbstractClassError } from "@aedart/support/exceptions"; import { classOwnKeys } from "@aedart/support/reflections"; @@ -63,22 +57,6 @@ export default abstract class AbstractConcern implements Concern return this.#concernOwner; } - /** - * @deprecated TODO: This must be removed again... To be replaced by [PROPERTIES]... - * - * Returns a list of properties and methods that MUST NOT be aliased into the target class. - * - * **Warning**: _Regardless of what properties and methods this method may return, - * an "injector" that injects this concern MUST ensure that the {@link ALWAYS_HIDDEN} - * defined properties and methods are **NEVER** aliased into a target class._ - * - * @return {ReadonlyArray} - */ - static [HIDDEN](): ReadonlyArray - { - return ALWAYS_HIDDEN; - } - /** * Returns list of property keys that this concern class offers. * diff --git a/tests/browser/packages/support/concerns/AbstractConcern.test.js b/tests/browser/packages/support/concerns/AbstractConcern.test.js index d194d280..d59d9b3a 100644 --- a/tests/browser/packages/support/concerns/AbstractConcern.test.js +++ b/tests/browser/packages/support/concerns/AbstractConcern.test.js @@ -1,5 +1,5 @@ import { AbstractConcern } from "@aedart/support/concerns"; -import { HIDDEN, PROVIDES } from "@aedart/contracts/support/concerns"; +import { PROVIDES } from "@aedart/contracts/support/concerns"; import { AbstractClassError } from "@aedart/support/exceptions"; describe('@aedart/support/concerns', () => { @@ -73,45 +73,5 @@ describe('@aedart/support/concerns', () => { .withContext('Incorrect properties for b') .toEqual([ 'constructor', 'concernOwner', 'foo', 'bar' ]); }); - - // TODO: To be removed - xit('returns default list of hidden properties and methods', () => { - - class MyConcern extends AbstractConcern {} - - const result = MyConcern[HIDDEN](); - - // Debug - // console.log('Hidden', result); - - expect(result.length) - .not - .toEqual(0) - }); - - // TODO: To be removed - xit('can overwrite default hidden', () => { - - const newHidden = [ 'a', 'b', 'c' ]; - class MyConcern extends AbstractConcern { - static [HIDDEN]() - { - return newHidden; - } - } - - // --------------------------------------------------------------- /7 - - const result = MyConcern[HIDDEN](); - for (const key of result) { - const k = typeof key == 'symbol' - ? key.toString() - : key; - - expect(newHidden.includes(key)) - .withContext(`${k} not part of HIDDEN`) - .toBeTrue() - } - }); }); }); \ No newline at end of file From 1b725cf8af3f57eac96a9c64b266667c986ef84e Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 09:38:58 +0100 Subject: [PATCH 265/424] Replace unknown type with any for call, set and get property methods --- .../src/support/concerns/Container.ts | 25 +++++++++++++------ .../support/src/concerns/ConcernsContainer.ts | 25 +++++++++++++------ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index ef9c2422..280e58f3 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -106,26 +106,34 @@ export default interface Container * * @param {Constructor} concern * @param {PropertyKey} method - * @param {...unknown} [args] + * @param {...any} [args] * - * @return {unknown} + * @return {any} * * @throws {ConcernException} * @throws {Error} */ - call(concern: Constructor, method: PropertyKey, ...args: unknown[]): unknown; + call( + concern: Constructor, + method: PropertyKey, + ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ /** * Set the value of given property in concern instance * * @param {Constructor} concern * @param {PropertyKey} property - * @param {unknown} value + * @param {any} value * * @throws {ConcernException} * @throws {Error} */ - setProperty(concern: Constructor, property: PropertyKey, value: unknown): void; + setProperty( + concern: Constructor, + property: PropertyKey, + value: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): void; /** * Get value of given property in concern instance @@ -133,10 +141,13 @@ export default interface Container * @param {Constructor} concern * @param {PropertyKey} property * - * @return {unknown} + * @return {any} * * @throws {ConcernException} * @throws {Error} */ - getProperty(concern: Constructor, property: PropertyKey): unknown; + getProperty( + concern: Constructor, + property: PropertyKey + ): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ } \ No newline at end of file diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts index 4e64f49a..c33d7d7d 100644 --- a/packages/support/src/concerns/ConcernsContainer.ts +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -209,14 +209,18 @@ export default class ConcernsContainer implements Container * * @param {Constructor} concern * @param {PropertyKey} method - * @param {...unknown} [args] + * @param {...any} [args] * - * @return {unknown} + * @return {any} * * @throws {ConcernError} * @throws {Error} */ - public call(concern: Constructor, method: PropertyKey, ...args: unknown[]): unknown + public call( + concern: Constructor, + method: PropertyKey, + ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { return this.get(concern)[method](...args); } @@ -226,12 +230,16 @@ export default class ConcernsContainer implements Container * * @param {Constructor} concern * @param {PropertyKey} property - * @param {unknown} value + * @param {any} value * * @throws {ConcernError} * @throws {Error} */ - public setProperty(concern: Constructor, property: PropertyKey, value: unknown): void + public setProperty( + concern: Constructor, + property: PropertyKey, + value: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): void { this.get(concern)[property] = value; } @@ -242,12 +250,15 @@ export default class ConcernsContainer implements Container * @param {Constructor} concern * @param {PropertyKey} property * - * @return {unknown} + * @return {any} * * @throws {ConcernError} * @throws {Error} */ - public getProperty(concern: Constructor, property: PropertyKey): unknown + public getProperty( + concern: Constructor, + property: PropertyKey + ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { return this.get(concern)[property]; } From f50ce9976b31022c6baa2b883fbb4cadb79f9c86 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 09:42:08 +0100 Subject: [PATCH 266/424] Add ts-expect-error for call, set and get property All of these methods can fail in any number of ways. TypeScript doesn't offer much help here, so we need to suppress its warnings. --- packages/support/src/concerns/ConcernsContainer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts index c33d7d7d..e4ee2d5b 100644 --- a/packages/support/src/concerns/ConcernsContainer.ts +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -222,6 +222,7 @@ export default class ConcernsContainer implements Container ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { + // @ts-expect-error Can fail when dynamically invoking method in concern instance... return this.get(concern)[method](...args); } @@ -241,6 +242,7 @@ export default class ConcernsContainer implements Container value: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ ): void { + // @ts-expect-error Can fail when dynamically retrieving property in concern instance... this.get(concern)[property] = value; } @@ -260,6 +262,7 @@ export default class ConcernsContainer implements Container property: PropertyKey ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { + // @ts-expect-error Can fail when dynamically setting property in concern instance... return this.get(concern)[property]; } } \ No newline at end of file From 06410b3b3dcba4ab7f70a948e6b09322a429f787 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 09:51:49 +0100 Subject: [PATCH 267/424] Change Container, use ConcernConstructor instead of Constructor --- .../src/support/concerns/Container.ts | 34 ++++++------- .../support/src/concerns/ConcernsContainer.ts | 48 +++++++++---------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index 280e58f3..32b99d68 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -1,4 +1,4 @@ -import type { Constructor } from "@aedart/contracts"; +import ConcernConstructor from "./ConcernConstructor"; import Concern from "./Concern"; import Owner from "./Owner"; @@ -26,11 +26,11 @@ export default interface Container /** * Determine if concern class is registered in this container * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * * @return {boolean} */ - has(concern: Constructor): boolean; + has(concern: ConcernConstructor): boolean; /** * Retrieve concern instance for given concern class @@ -41,37 +41,37 @@ export default interface Container * * @template T extends {@link Concern} * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * * @return {Concern} The booted instance of the concern class. If concern class was * previously booted, then that instance is returned. * * @throws {ConcernException} */ - get(concern: Constructor): T; + get(concern: ConcernConstructor): T; /** * Determine if concern class has been booted * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * * @return {boolean} */ - hasBooted(concern: Constructor): boolean + hasBooted(concern: ConcernConstructor): boolean /** * Boot concern class * * @template T extends {@link Concern} * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * * @return {Concern} New concern instance * * @throws {NotRegisteredException} If concern class is not registered in this container * @throws {BootException} If concern is unable to be booted, e.g. if already booted */ - boot(concern: Constructor): T; + boot(concern: ConcernConstructor): T; /** * Boots all registered concern classes @@ -97,14 +97,14 @@ export default interface Container /** * Returns all concern classes * - * @return {IterableIterator>} + * @return {IterableIterator} */ - all(): IterableIterator>; + all(): IterableIterator; /** * Invoke a method with given arguments in concern instance * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * @param {PropertyKey} method * @param {...any} [args] * @@ -114,7 +114,7 @@ export default interface Container * @throws {Error} */ call( - concern: Constructor, + concern: ConcernConstructor, method: PropertyKey, ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ ): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ @@ -122,7 +122,7 @@ export default interface Container /** * Set the value of given property in concern instance * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * @param {PropertyKey} property * @param {any} value * @@ -130,7 +130,7 @@ export default interface Container * @throws {Error} */ setProperty( - concern: Constructor, + concern: ConcernConstructor, property: PropertyKey, value: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ ): void; @@ -138,7 +138,7 @@ export default interface Container /** * Get value of given property in concern instance * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * @param {PropertyKey} property * * @return {any} @@ -147,7 +147,7 @@ export default interface Container * @throws {Error} */ getProperty( - concern: Constructor, + concern: ConcernConstructor, property: PropertyKey ): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ } \ No newline at end of file diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts index e4ee2d5b..8d7d2f5d 100644 --- a/packages/support/src/concerns/ConcernsContainer.ts +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -1,9 +1,9 @@ import type { Container, Concern, + ConcernConstructor, Owner } from "@aedart/contracts/support/concerns"; -import type { Constructor } from "@aedart/contracts"; import { getNameOrDesc } from "@aedart/support/reflections"; import BootError from "./exceptions/BootError"; import NotRegisteredError from "./exceptions/NotRegisteredError"; @@ -22,9 +22,9 @@ export default class ConcernsContainer implements Container * @private * @readonly * - * @type {Map, Concern|undefined>} + * @type {Map} */ - readonly #map: Map, Concern|undefined>; + readonly #map: Map; /** * The concerns owner of this container @@ -40,13 +40,13 @@ export default class ConcernsContainer implements Container * Create a new Concerns Container instance * * @param {Owner} owner - * @param {Constructor[]} concerns + * @param {ConcernConstructor[]} concerns */ - public constructor(owner: Owner, concerns: Constructor[]) { + public constructor(owner: Owner, concerns: ConcernConstructor[]) { this.#owner = owner; - this.#map = new Map, Concern | undefined>(); + this.#map = new Map(); - for(const concern: Constructor of concerns) { + for(const concern of concerns) { this.#map.set(concern, undefined); } } @@ -78,11 +78,11 @@ export default class ConcernsContainer implements Container /** * Determine if concern class is registered in this container * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * * @return {boolean} */ - public has(concern: Constructor): boolean + public has(concern: ConcernConstructor): boolean { return this.#map.has(concern); } @@ -103,7 +103,7 @@ export default class ConcernsContainer implements Container * * @throws {ConcernError} */ - public get(concern: Constructor): T + public get(concern: ConcernConstructor): T { if (!this.hasBooted(concern)) { return this.boot(concern); @@ -115,11 +115,11 @@ export default class ConcernsContainer implements Container /** * Determine if concern class has been booted * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * * @return {boolean} */ - public hasBooted(concern: Constructor): boolean + public hasBooted(concern: ConcernConstructor): boolean { return this.has(concern) && this.#map.get(concern) !== undefined; } @@ -129,14 +129,14 @@ export default class ConcernsContainer implements Container * * @template T extends {@link Concern} * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * * @return {Concern} New concern instance * * @throws {NotRegisteredError} If concern class is not registered in this container * @throws {BootError} If concern is unable to be booted, e.g. if already booted */ - public boot(concern: Constructor): T + public boot(concern: ConcernConstructor): T { // Fail if given concern is not in this container if (!this.has(concern)) { @@ -168,8 +168,8 @@ export default class ConcernsContainer implements Container */ public bootAll(): void { - const concerns: IterableIterator> = this.all(); - for (const concern: Constructor of concerns) { + const concerns = this.all(); + for (const concern of concerns) { this.boot(concern); } } @@ -197,9 +197,9 @@ export default class ConcernsContainer implements Container /** * Returns all concern classes * - * @return {IterableIterator>} + * @return {IterableIterator} */ - public all(): IterableIterator> + public all(): IterableIterator { return this.#map.keys(); } @@ -207,7 +207,7 @@ export default class ConcernsContainer implements Container /** * Invoke a method with given arguments in concern instance * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * @param {PropertyKey} method * @param {...any} [args] * @@ -217,7 +217,7 @@ export default class ConcernsContainer implements Container * @throws {Error} */ public call( - concern: Constructor, + concern: ConcernConstructor, method: PropertyKey, ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ @@ -229,7 +229,7 @@ export default class ConcernsContainer implements Container /** * Set the value of given property in concern instance * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * @param {PropertyKey} property * @param {any} value * @@ -237,7 +237,7 @@ export default class ConcernsContainer implements Container * @throws {Error} */ public setProperty( - concern: Constructor, + concern: ConcernConstructor, property: PropertyKey, value: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ ): void @@ -249,7 +249,7 @@ export default class ConcernsContainer implements Container /** * Get value of given property in concern instance * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * @param {PropertyKey} property * * @return {any} @@ -258,7 +258,7 @@ export default class ConcernsContainer implements Container * @throws {Error} */ public getProperty( - concern: Constructor, + concern: ConcernConstructor, property: PropertyKey ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { From d600d94ef1ea35bb4260b512466779fa821a1efb Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 09:54:28 +0100 Subject: [PATCH 268/424] Replace unknown with any type --- packages/support/src/exceptions/getErrorMessage.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/support/src/exceptions/getErrorMessage.ts b/packages/support/src/exceptions/getErrorMessage.ts index 8db2ecc0..36610ca1 100644 --- a/packages/support/src/exceptions/getErrorMessage.ts +++ b/packages/support/src/exceptions/getErrorMessage.ts @@ -1,12 +1,15 @@ /** * Returns error message from {@link Error}, if possible * - * @param {unknown} error Error or value that was thrown + * @param {any} error Error or value that was thrown * @param {string} [defaultMessage] A default message to return if unable to resolve error message * * @return {string} */ -export function getErrorMessage(error: unknown, defaultMessage: string = 'unknown reason'): string +export function getErrorMessage( + error: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + defaultMessage: string = 'unknown reason' +): string { return (typeof error == 'object' && error instanceof Error && Reflect.has(error, 'message')) ? error.message From 74fd854cb17db05c5fd24e148855df112d9ca45b Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 09:57:06 +0100 Subject: [PATCH 269/424] Use getErrorMessage() for resolving error reason --- packages/support/src/concerns/ConcernsContainer.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts index 8d7d2f5d..926b2f6b 100644 --- a/packages/support/src/concerns/ConcernsContainer.ts +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -5,6 +5,7 @@ import type { Owner } from "@aedart/contracts/support/concerns"; import { getNameOrDesc } from "@aedart/support/reflections"; +import { getErrorMessage } from "@aedart/support/exceptions"; import BootError from "./exceptions/BootError"; import NotRegisteredError from "./exceptions/NotRegisteredError"; @@ -146,7 +147,11 @@ export default class ConcernsContainer implements Container // Fail if concern instance already exists (has booted) let instance: T | undefined = this.#map.get(concern) as T | undefined; if (instance !== undefined) { - throw new BootError(concern, `Concern ${getNameOrDesc(concern)} is already booted`, { cause: { owner: this.owner } }); + throw new BootError( + concern, + `Concern ${getNameOrDesc(concern)} is already booted`, + { cause: { owner: this.owner } } + ); } // Boot the concern (create new instance) and register it... @@ -154,8 +159,11 @@ export default class ConcernsContainer implements Container instance = new concern(this.owner); this.#map.set(concern, instance); } catch (error) { - const reason: string = error?.message || 'unknown reason!'; - throw new BootError(concern, `Unable to boot concern ${getNameOrDesc(concern)}: ${reason}`, { cause: { previous: error, owner: this.owner } }); + throw new BootError( + concern, + `Unable to boot concern ${getNameOrDesc(concern)}: ${getErrorMessage(error)}`, + { cause: { previous: error, owner: this.owner } } + ); } return instance; From efee39d96423b1df62c278112cf3a2ae6b176d55 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 10:02:03 +0100 Subject: [PATCH 270/424] Use ConcernConstructor instead of Constructor --- .../concerns/exceptions/ConcernException.ts | 7 +++---- .../support/src/concerns/exceptions/BootError.ts | 7 +++---- .../src/concerns/exceptions/ConcernError.ts | 15 +++++++-------- .../src/concerns/exceptions/InjectionError.ts | 10 +++++----- .../src/concerns/exceptions/NotRegisteredError.ts | 7 +++---- 5 files changed, 21 insertions(+), 25 deletions(-) diff --git a/packages/contracts/src/support/concerns/exceptions/ConcernException.ts b/packages/contracts/src/support/concerns/exceptions/ConcernException.ts index f78269c6..13f57a05 100644 --- a/packages/contracts/src/support/concerns/exceptions/ConcernException.ts +++ b/packages/contracts/src/support/concerns/exceptions/ConcernException.ts @@ -1,6 +1,5 @@ import { Throwable } from "@aedart/contracts/support/exceptions"; -import { Constructor } from "@aedart/contracts"; -import Concern from "../Concern"; +import ConcernConstructor from '../ConcernConstructor' /** * Concern Exception @@ -14,7 +13,7 @@ export default interface ConcernException extends Throwable { * * @readonly * - * @type {Constructor} + * @type {ConcernConstructor} */ - readonly concern: Constructor + readonly concern: ConcernConstructor } \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/BootError.ts b/packages/support/src/concerns/exceptions/BootError.ts index 982a50b5..b81ff0e0 100644 --- a/packages/support/src/concerns/exceptions/BootError.ts +++ b/packages/support/src/concerns/exceptions/BootError.ts @@ -1,5 +1,4 @@ -import type { BootException, Concern } from "@aedart/contracts/support/concerns"; -import type { Constructor} from "@aedart/contracts"; +import type { BootException, ConcernConstructor } from "@aedart/contracts/support/concerns"; import ConcernError from "./ConcernError"; import { configureCustomError } from "@aedart/support/exceptions"; @@ -13,11 +12,11 @@ export default class BootError extends ConcernError implements BootException /** * Create a new Concern Boot Error instance * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * @param {string} message * @param {ErrorOptions} [options] */ - constructor(concern: Constructor, message: string, options?: ErrorOptions) + constructor(concern: ConcernConstructor, message: string, options?: ErrorOptions) { super(concern, message, options); diff --git a/packages/support/src/concerns/exceptions/ConcernError.ts b/packages/support/src/concerns/exceptions/ConcernError.ts index cfd6cb8e..1a5d863b 100644 --- a/packages/support/src/concerns/exceptions/ConcernError.ts +++ b/packages/support/src/concerns/exceptions/ConcernError.ts @@ -1,5 +1,4 @@ -import type { ConcernException, Concern } from "@aedart/contracts/support/concerns"; -import type { Constructor } from "@aedart/contracts"; +import type { ConcernException, ConcernConstructor } from "@aedart/contracts/support/concerns"; import { configureCustomError } from "@aedart/support/exceptions"; /** @@ -14,18 +13,18 @@ export default class ConcernError extends Error implements ConcernException * * @private * - * @type {Constructor} + * @type {ConcernConstructor} */ - readonly #concern: Constructor + readonly #concern: ConcernConstructor /** * Create a new Concern Error instance * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * @param {string} message * @param {ErrorOptions} [options] */ - constructor(concern: Constructor, message: string, options?: ErrorOptions) + constructor(concern: ConcernConstructor, message: string, options?: ErrorOptions) { super(message, options || { cause: {} }); @@ -42,9 +41,9 @@ export default class ConcernError extends Error implements ConcernException * * @readonly * - * @type {Constructor} + * @type {ConcernConstructor} */ - get concern(): Constructor + get concern(): ConcernConstructor { return this.#concern; } diff --git a/packages/support/src/concerns/exceptions/InjectionError.ts b/packages/support/src/concerns/exceptions/InjectionError.ts index b894a0b6..f4783fcc 100644 --- a/packages/support/src/concerns/exceptions/InjectionError.ts +++ b/packages/support/src/concerns/exceptions/InjectionError.ts @@ -1,7 +1,7 @@ -import ConcernError from "./ConcernError"; -import { Concern, InjectionException, MustUseConcerns } from "@aedart/contracts/support/concerns"; -import type { Constructor, ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConcernConstructor, InjectionException, MustUseConcerns } from "@aedart/contracts/support/concerns"; +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import { configureCustomError } from "@aedart/support/exceptions"; +import ConcernError from "./ConcernError"; /** * Injection Error @@ -23,13 +23,13 @@ export default class InjectionError extends ConcernError implements InjectionExc * Create a new Injection Error instance * * @param {ConstructorOrAbstractConstructor | MustUseConcerns} target - * @param {Constructor} concern + * @param {ConcernConstructor} concern * @param {string} message * @param {ErrorOptions} [options] */ constructor( target: ConstructorOrAbstractConstructor | MustUseConcerns, - concern: Constructor, + concern: ConcernConstructor, message: string, options?: ErrorOptions ) { diff --git a/packages/support/src/concerns/exceptions/NotRegisteredError.ts b/packages/support/src/concerns/exceptions/NotRegisteredError.ts index 7238ad65..dbfc938f 100644 --- a/packages/support/src/concerns/exceptions/NotRegisteredError.ts +++ b/packages/support/src/concerns/exceptions/NotRegisteredError.ts @@ -1,5 +1,4 @@ -import type { Concern, NotRegisteredException } from "@aedart/contracts/support/concerns"; -import type { Constructor } from "@aedart/contracts"; +import type { ConcernConstructor, NotRegisteredException } from "@aedart/contracts/support/concerns"; import ConcernError from './ConcernError'; import { getNameOrDesc } from "@aedart/support/reflections"; import { configureCustomError } from "@aedart/support/exceptions"; @@ -14,10 +13,10 @@ export default class NotRegisteredError extends ConcernError implements NotRegis /** * Create a new Concern Not Registered Error instance * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * @param {ErrorOptions} [options] */ - constructor(concern: Constructor, options?: ErrorOptions) + constructor(concern: ConcernConstructor, options?: ErrorOptions) { super(concern, `Concern ${getNameOrDesc(concern)} is not registered in concerns container`, options); From c27a01686a88de7068fbbf02d377ab6f62011e04 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 10:08:19 +0100 Subject: [PATCH 271/424] Change internal map visibility to protected This could become handy for developers that wish to create their own extended version of a concerns container. Really wish that JavaScript supported "protected" visibility and not just private / public. --- .../support/src/concerns/ConcernsContainer.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts index 926b2f6b..b458b202 100644 --- a/packages/support/src/concerns/ConcernsContainer.ts +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -17,15 +17,14 @@ import NotRegisteredError from "./exceptions/NotRegisteredError"; export default class ConcernsContainer implements Container { /** - * Map that holds concern class constructors - * and actual concern instances + * Map of concern class constructors and actual concern instances * - * @private + * @protected * @readonly * * @type {Map} */ - readonly #map: Map; + protected readonly map: Map; /** * The concerns owner of this container @@ -45,10 +44,10 @@ export default class ConcernsContainer implements Container */ public constructor(owner: Owner, concerns: ConcernConstructor[]) { this.#owner = owner; - this.#map = new Map(); + this.map = new Map(); for(const concern of concerns) { - this.#map.set(concern, undefined); + this.map.set(concern, undefined); } } @@ -61,7 +60,7 @@ export default class ConcernsContainer implements Container */ public get size(): number { - return this.#map.size; + return this.map.size; } /** @@ -85,7 +84,7 @@ export default class ConcernsContainer implements Container */ public has(concern: ConcernConstructor): boolean { - return this.#map.has(concern); + return this.map.has(concern); } /** @@ -110,7 +109,7 @@ export default class ConcernsContainer implements Container return this.boot(concern); } - return this.#map.get(concern) as T; + return this.map.get(concern) as T; } /** @@ -122,7 +121,7 @@ export default class ConcernsContainer implements Container */ public hasBooted(concern: ConcernConstructor): boolean { - return this.has(concern) && this.#map.get(concern) !== undefined; + return this.has(concern) && this.map.get(concern) !== undefined; } /** @@ -145,7 +144,7 @@ export default class ConcernsContainer implements Container } // Fail if concern instance already exists (has booted) - let instance: T | undefined = this.#map.get(concern) as T | undefined; + let instance: T | undefined = this.map.get(concern) as T | undefined; if (instance !== undefined) { throw new BootError( concern, @@ -157,7 +156,7 @@ export default class ConcernsContainer implements Container // Boot the concern (create new instance) and register it... try { instance = new concern(this.owner); - this.#map.set(concern, instance); + this.map.set(concern, instance); } catch (error) { throw new BootError( concern, @@ -209,7 +208,7 @@ export default class ConcernsContainer implements Container */ public all(): IterableIterator { - return this.#map.keys(); + return this.map.keys(); } /** From bef7ac92c370145c89ef4102098bde79026decbe Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 11:03:28 +0100 Subject: [PATCH 272/424] Change PROVIDES symbol description --- packages/contracts/src/support/concerns/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 5a62b74a..fe53e989 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -53,7 +53,7 @@ export const HIDDEN: unique symbol = Symbol('hidden'); * * @type {symbol} */ -export const PROVIDES : unique symbol = Symbol('concern_properties'); +export const PROVIDES : unique symbol = Symbol('concern_provides'); /** * Symbol used to define a list of the concern classes that a given target class From f9343136474323a3a59541bb73525f4743189fde Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 11:08:19 +0100 Subject: [PATCH 273/424] Rename isConernClass to isConcernConstructor --- packages/support/src/concerns/index.ts | 2 +- .../support/src/concerns/isConcernClass.ts | 47 ------------- .../src/concerns/isConcernConstructor.ts | 68 +++++++++++++++++++ .../support/concerns/isConcernClass.test.js | 56 --------------- .../concerns/isConcernConstructor.test.js | 47 +++++++++++++ 5 files changed, 116 insertions(+), 104 deletions(-) delete mode 100644 packages/support/src/concerns/isConcernClass.ts create mode 100644 packages/support/src/concerns/isConcernConstructor.ts delete mode 100644 tests/browser/packages/support/concerns/isConcernClass.test.js create mode 100644 tests/browser/packages/support/concerns/isConcernConstructor.test.js diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 026d2421..5ca8dd90 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -9,5 +9,5 @@ export { }; export * from './exceptions'; -export * from './isConcernClass'; +export * from './isConcernConstructor'; export * from './use'; \ No newline at end of file diff --git a/packages/support/src/concerns/isConcernClass.ts b/packages/support/src/concerns/isConcernClass.ts deleted file mode 100644 index fa399df3..00000000 --- a/packages/support/src/concerns/isConcernClass.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { isSubclass, getClassPropertyDescriptors } from "@aedart/support/reflections"; -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; -import AbstractConcern from "./AbstractConcern"; - -/** - * In-memory cache of classes that are determined to be of the type [Concern]{@link import('@aedart/contracts/support/concerns').Concern} - * - * @type {WeakSet} - */ -const concernClassCache: WeakSet = new WeakSet(); - -/** - * TODO: INCOMPLETE - * - * Determine if given target is a [Concern]{@link import('@aedart/contracts/support/concerns').Concern} class - * - * @param {object} target - * @param {boolean} [force=false] If `false` then cached result is returned - * - * @returns {boolean} - */ -export function isConcernClass(target: object, force: boolean = false): boolean -{ - if (!force && concernClassCache.has(target)) { - return true; - } - - if (isSubclass(target, AbstractConcern)) { - concernClassCache.add(parent); - return true; - } - - try { - // TODO: THIS MUST CHANGE - its way too heavy to obtain class property descriptors here... Use hasAllMethods() on class prototype instead, or similar... - - const descriptors = getClassPropertyDescriptors(target as ConstructorOrAbstractConstructor, true); - - if (Reflect.has(descriptors, 'concernOwner')) { - concernClassCache.add(parent); - return true; - } - - return false; - } catch (error) { - return false; - } -} \ No newline at end of file diff --git a/packages/support/src/concerns/isConcernConstructor.ts b/packages/support/src/concerns/isConcernConstructor.ts new file mode 100644 index 00000000..46b0ed2a --- /dev/null +++ b/packages/support/src/concerns/isConcernConstructor.ts @@ -0,0 +1,68 @@ +import {isSubclass, getClassPropertyDescriptors, classOwnKeys} from "@aedart/support/reflections"; +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { PROVIDES } from "@aedart/contracts/support/concerns"; +import { isset } from "@aedart/support/misc"; +import AbstractConcern from "./AbstractConcern"; + +/** + * In-memory cache of classes that are determined to be of the type [Concern Constructor]{@link import('@aedart/contracts/support/concerns').ConcernConstructor} + * + * @type {WeakSet} + */ +const concernClassCache: WeakSet = new WeakSet(); + +/** + * TODO: INCOMPLETE + * + * Determine if given target is a [Concern Constructor]{@link import('@aedart/contracts/support/concerns').ConcernConstructor} + * + * @param {object} target + * @param {boolean} [force=false] If `false` then cached result is returned + * + * @returns {boolean} + */ +export function isConcernConstructor(target: object, force: boolean = false): boolean +{ + if (!isset(target) || typeof target !== 'object' || Array.isArray(target)) { + return false; + } + + if (!force && concernClassCache.has(target)) { + return true; + } + + // Easiest way to determine is be checking if target is a subclass of Abstract Concern + if (isSubclass(target, AbstractConcern)) { + concernClassCache.add(target); + return true; + } + + // If a custom implementation is used, then certain static members must be available + const requiredStaticMembers: PropertyKey[] = [ + PROVIDES + ]; + + const staticKeys: PropertyKey[] = Reflect.ownKeys(target); // TODO: BAD... what about inherited??? + for (const staticMember of requiredStaticMembers) { + if (!staticKeys.includes(staticMember)) { + console.log('Does not contain', staticMember, 'in', target, ' - static members', staticKeys); + return false; + } + } + + // Secondly, the custom concern class must also have certain members available on its prototype + const requiredMembers: PropertyKey[] = [ + 'concernOwner' + ]; + + const classKeys: PropertyKey[] = classOwnKeys(target as ConstructorOrAbstractConstructor, true); + for (const member of requiredMembers) { + if (!classKeys.includes(member)) { + return false; + } + } + + // Thus, if all required members are available then the given target is a valid concern class. + concernClassCache.add(target); + return true; +} \ No newline at end of file diff --git a/tests/browser/packages/support/concerns/isConcernClass.test.js b/tests/browser/packages/support/concerns/isConcernClass.test.js deleted file mode 100644 index 089ee96c..00000000 --- a/tests/browser/packages/support/concerns/isConcernClass.test.js +++ /dev/null @@ -1,56 +0,0 @@ -import { isConcernClass, AbstractConcern } from "@aedart/support/concerns"; - -describe('@aedart/support/concerns', () => { - describe('isConcernClass()', () => { - - it('can determine if is a concern class', () => { - - const arr = []; - - class A {} - - class B { - get concernOwner() { - return false; - } - } - - class C extends AbstractConcern {} - - class D extends C {} - - class E extends B {} - - // ------------------------------------------------------------------------------------ // - - expect(isConcernClass(null)) - .withContext('Null is not a concern class') - .toBeFalse(); - - expect(isConcernClass(arr)) - .withContext('Array is not a concern class') - .toBeFalse(); - - expect(isConcernClass(A)) - .withContext('Class A is not a concern class') - .toBeFalse(); - - expect(isConcernClass(B)) - .withContext('Class B should be considered concern class') - .toBeTrue(); - - expect(isConcernClass(C)) - .withContext('Class C is concern class') - .toBeTrue(); - - expect(isConcernClass(D)) - .withContext('Class D is concern class (inherits from Class C)') - .toBeTrue(); - - expect(isConcernClass(E)) - .withContext('Class E should be considered a concern class (inherits from Class B)') - .toBeTrue(); - }); - - }); -}); \ No newline at end of file diff --git a/tests/browser/packages/support/concerns/isConcernConstructor.test.js b/tests/browser/packages/support/concerns/isConcernConstructor.test.js new file mode 100644 index 00000000..de301b56 --- /dev/null +++ b/tests/browser/packages/support/concerns/isConcernConstructor.test.js @@ -0,0 +1,47 @@ +import { PROVIDES } from "@aedart/contracts/support/concerns"; +import { isConcernConstructor, AbstractConcern } from "@aedart/support/concerns"; + +describe('@aedart/support/concerns', () => { + describe('isConcernConstructor()', () => { + + it('can determine if is a concern class', () => { + + class A {} + + class B { + get concernOwner() { + return false; + } + + static [PROVIDES]() {} + } + + class C extends B {} + + class D extends AbstractConcern {} + + class E extends D {} + + // ------------------------------------------------------------------------------------ // + + const data = [ + { value: null, expected: false, name: 'Null' }, + { value: [], expected: false, name: 'Array' }, + { value: {}, expected: false, name: 'Object (empty)' }, + { value: A, expected: false, name: 'Class A (empty)' }, + + { value: B, expected: true, name: 'Class B (custom implementation of concern)' }, + { value: C, expected: true, name: 'Class C (inherits from custom implementation)' }, + { value: D, expected: true, name: 'Class D (inherits from AbstractConcern)' }, + { value: E, expected: true, name: 'Class E (inherits from a base that inherits from AbstractConcern)' }, + ]; + + for (const entry of data) { + expect(isConcernClass(entry.value)) + .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) + .toBe(entry.expected); + } + }); + + }); +}); \ No newline at end of file From c1185bf69b33f1115b6bd227fd99395b9ac44c1f Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 11:32:23 +0100 Subject: [PATCH 274/424] Redesign isConcernConstructor() util function --- .../src/concerns/isConcernConstructor.ts | 54 +++++++++---------- .../concerns/isConcernConstructor.test.js | 6 +-- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/packages/support/src/concerns/isConcernConstructor.ts b/packages/support/src/concerns/isConcernConstructor.ts index 46b0ed2a..34b28566 100644 --- a/packages/support/src/concerns/isConcernConstructor.ts +++ b/packages/support/src/concerns/isConcernConstructor.ts @@ -1,68 +1,66 @@ -import {isSubclass, getClassPropertyDescriptors, classOwnKeys} from "@aedart/support/reflections"; +import {isSubclass, classOwnKeys} from "@aedart/support/reflections"; import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import { PROVIDES } from "@aedart/contracts/support/concerns"; import { isset } from "@aedart/support/misc"; import AbstractConcern from "./AbstractConcern"; /** - * In-memory cache of classes that are determined to be of the type [Concern Constructor]{@link import('@aedart/contracts/support/concerns').ConcernConstructor} + * In-memory cache of classes that are determined to be of the type + * [Concern Constructor]{@link import('@aedart/contracts/support/concerns').ConcernConstructor}. * * @type {WeakSet} */ -const concernClassCache: WeakSet = new WeakSet(); +const concernConstructorsCache: WeakSet = new WeakSet(); /** - * TODO: INCOMPLETE + * Determine if given target is a + * [Concern Constructor]{@link import('@aedart/contracts/support/concerns').ConcernConstructor}. * - * Determine if given target is a [Concern Constructor]{@link import('@aedart/contracts/support/concerns').ConcernConstructor} - * - * @param {object} target - * @param {boolean} [force=false] If `false` then cached result is returned + * @param {any} target + * @param {boolean} [force=false] If `false` then cached result is returned if available. * * @returns {boolean} */ -export function isConcernConstructor(target: object, force: boolean = false): boolean +export function isConcernConstructor( + target: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + force: boolean = false +): boolean { - if (!isset(target) || typeof target !== 'object' || Array.isArray(target)) { + if (!isset(target) || typeof target !== 'function') { return false; } - if (!force && concernClassCache.has(target)) { + if (!force && concernConstructorsCache.has(target)) { return true; } // Easiest way to determine is be checking if target is a subclass of Abstract Concern if (isSubclass(target, AbstractConcern)) { - concernClassCache.add(target); + concernConstructorsCache.add(target); return true; } - // If a custom implementation is used, then certain static members must be available - const requiredStaticMembers: PropertyKey[] = [ - PROVIDES - ]; + // However, if given target is a custom implementation, then there are a few members + // that MUST be available, before it can be considered a concern constructor + const staticMembers: PropertyKey[] = [ PROVIDES ]; + const members: PropertyKey[] = [ 'concernOwner' ]; - const staticKeys: PropertyKey[] = Reflect.ownKeys(target); // TODO: BAD... what about inherited??? - for (const staticMember of requiredStaticMembers) { - if (!staticKeys.includes(staticMember)) { - console.log('Does not contain', staticMember, 'in', target, ' - static members', staticKeys); + // Abort if static members are not available... + for (const staticMember of staticMembers) { + if (!Reflect.has(target as object, staticMember)) { return false; } } - - // Secondly, the custom concern class must also have certain members available on its prototype - const requiredMembers: PropertyKey[] = [ - 'concernOwner' - ]; - + + // Abort if members are not available... const classKeys: PropertyKey[] = classOwnKeys(target as ConstructorOrAbstractConstructor, true); - for (const member of requiredMembers) { + for (const member of members) { if (!classKeys.includes(member)) { return false; } } // Thus, if all required members are available then the given target is a valid concern class. - concernClassCache.add(target); + concernConstructorsCache.add(target); return true; } \ No newline at end of file diff --git a/tests/browser/packages/support/concerns/isConcernConstructor.test.js b/tests/browser/packages/support/concerns/isConcernConstructor.test.js index de301b56..ca03d84f 100644 --- a/tests/browser/packages/support/concerns/isConcernConstructor.test.js +++ b/tests/browser/packages/support/concerns/isConcernConstructor.test.js @@ -4,7 +4,7 @@ import { isConcernConstructor, AbstractConcern } from "@aedart/support/concerns" describe('@aedart/support/concerns', () => { describe('isConcernConstructor()', () => { - it('can determine if is a concern class', () => { + it('can determine if target is a concern constructor', () => { class A {} @@ -29,7 +29,7 @@ describe('@aedart/support/concerns', () => { { value: [], expected: false, name: 'Array' }, { value: {}, expected: false, name: 'Object (empty)' }, { value: A, expected: false, name: 'Class A (empty)' }, - + { value: B, expected: true, name: 'Class B (custom implementation of concern)' }, { value: C, expected: true, name: 'Class C (inherits from custom implementation)' }, { value: D, expected: true, name: 'Class D (inherits from AbstractConcern)' }, @@ -37,7 +37,7 @@ describe('@aedart/support/concerns', () => { ]; for (const entry of data) { - expect(isConcernClass(entry.value)) + expect(isConcernConstructor(entry.value)) .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) .toBe(entry.expected); } From 31653463e3e290d482619fb40bc62890587dfb80 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 12:05:52 +0100 Subject: [PATCH 275/424] Add includesAny() and includesAll() util functions for arrays --- packages/support/src/arrays/includesAll.ts | 15 +++++ packages/support/src/arrays/includesAny.ts | 15 +++++ packages/support/src/arrays/index.ts | 2 + .../support/arrays/includes-any-all.test.js | 55 +++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 packages/support/src/arrays/includesAll.ts create mode 100644 packages/support/src/arrays/includesAny.ts create mode 100644 tests/browser/packages/support/arrays/includes-any-all.test.js diff --git a/packages/support/src/arrays/includesAll.ts b/packages/support/src/arrays/includesAll.ts new file mode 100644 index 00000000..e052e60b --- /dev/null +++ b/packages/support/src/arrays/includesAll.ts @@ -0,0 +1,15 @@ +/** + * Determine if array includes all given values + * + * @param {any[]} arr + * @param {any[]} values + * + * @return {boolean} + */ +export function includesAll( + arr: any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + values: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ +): boolean +{ + return values.every((value) => arr.includes(value)); +} \ No newline at end of file diff --git a/packages/support/src/arrays/includesAny.ts b/packages/support/src/arrays/includesAny.ts new file mode 100644 index 00000000..7fa46a25 --- /dev/null +++ b/packages/support/src/arrays/includesAny.ts @@ -0,0 +1,15 @@ +/** + * Determine if array includes any (_some_) of the given values + * + * @param {any[]} arr + * @param {any[]} values + * + * @return {boolean} + */ +export function includesAny( + arr: any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + values: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ +): boolean +{ + return values.some((value) => arr.includes(value)); +} \ No newline at end of file diff --git a/packages/support/src/arrays/index.ts b/packages/support/src/arrays/index.ts index 99de386d..5f3aed30 100644 --- a/packages/support/src/arrays/index.ts +++ b/packages/support/src/arrays/index.ts @@ -1,3 +1,5 @@ +export * from './includesAll'; +export * from './includesAny'; export * from './isArrayLike'; export * from './isConcatSpreadable'; export * from './isSafeArrayLike'; diff --git a/tests/browser/packages/support/arrays/includes-any-all.test.js b/tests/browser/packages/support/arrays/includes-any-all.test.js new file mode 100644 index 00000000..3ba30b4f --- /dev/null +++ b/tests/browser/packages/support/arrays/includes-any-all.test.js @@ -0,0 +1,55 @@ +import { includesAll, includesAny } from "@aedart/support/arrays"; + +describe('@aedart/support/arrays', () => { + describe('includesAll()', () => { + + it('can determine if array includes all values', () => { + const data = [ + { + arr: [ 1, 2, 3], + values: [ 1, 2 ], + expected: true, + name: 'A' + }, + { + arr: [ 1, 2, 3], + values: [ 1, 4 ], + expected: false, + name: 'B' + }, + ]; + + for (const entry of data) { + expect(includesAll(entry.arr, entry.values)) + .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) + .toBe(entry.expected); + } + }); + }); + + describe('includesAny()', () => { + + it('can determine if array includes any (some) values', () => { + const data = [ + { + arr: [ 1, 2, 3], + values: [ 4, 2 ], + expected: true, + name: 'A' + }, + { + arr: [ 1, 2, 3], + values: [ 4, 5 ], + expected: false, + name: 'B' + }, + ]; + + for (const entry of data) { + expect(includesAny(entry.arr, entry.values)) + .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) + .toBe(entry.expected); + } + }); + }); +}); \ No newline at end of file From 5c0f4e8743d760004de8720708043e694329df9f Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 12:06:00 +0100 Subject: [PATCH 276/424] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e9938e4..c0534a01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `merge()`, `populate()`, `isCloneable()` and `isPopulatable()` in `@aedart/support/objects`. * Objects `Merger` (_underlying component for the objects `merge()` util_) in `@aedart/support/objects`. * `merge()`, `isTypedArray()`, `isArrayLike()`, `isSafeArrayLike()`, `isTypedArray()` and `isConcatSpreadable()` in `@aedart/support/arrays`. +* `includesAny()` and `includesAll()` in `@aedart/support/arrays`. ### Fixed From 27f97bb2966e1175c753d611e6415bd61ba26330 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 12:12:47 +0100 Subject: [PATCH 277/424] Add ClassBlueprint interface --- .../src/support/reflections/ClassBlueprint.ts | 19 +++++++++++++++++++ .../src/support/reflections/index.ts | 5 +++++ 2 files changed, 24 insertions(+) create mode 100644 packages/contracts/src/support/reflections/ClassBlueprint.ts diff --git a/packages/contracts/src/support/reflections/ClassBlueprint.ts b/packages/contracts/src/support/reflections/ClassBlueprint.ts new file mode 100644 index 00000000..6aa71ead --- /dev/null +++ b/packages/contracts/src/support/reflections/ClassBlueprint.ts @@ -0,0 +1,19 @@ +/** + * Class Blueprint + */ +export default interface ClassBlueprint +{ + /** + * Properties or methods that are statically defined in class + * + * @type {PropertyKey[]} + */ + staticMembers?: PropertyKey[]; + + /** + * Properties or methods defined on class' prototype + * + * @type {PropertyKey[]} + */ + members: PropertyKey[]; +} \ No newline at end of file diff --git a/packages/contracts/src/support/reflections/index.ts b/packages/contracts/src/support/reflections/index.ts index 7d9f122f..e91fe56c 100644 --- a/packages/contracts/src/support/reflections/index.ts +++ b/packages/contracts/src/support/reflections/index.ts @@ -26,3 +26,8 @@ export const FUNCTION_PROTOTYPE: object = Reflect.getPrototypeOf(Function) as ob * @type {object} */ export const TYPED_ARRAY_PROTOTYPE: object = Reflect.getPrototypeOf(Int8Array) as object; + +import ClassBlueprint from "./ClassBlueprint"; +export { + type ClassBlueprint +} From 9bc86ecfe1567aeb560ddd986d3e7448f9026ea6 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 13:00:56 +0100 Subject: [PATCH 278/424] Change members property to be optional --- packages/contracts/src/support/reflections/ClassBlueprint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/reflections/ClassBlueprint.ts b/packages/contracts/src/support/reflections/ClassBlueprint.ts index 6aa71ead..18943de8 100644 --- a/packages/contracts/src/support/reflections/ClassBlueprint.ts +++ b/packages/contracts/src/support/reflections/ClassBlueprint.ts @@ -15,5 +15,5 @@ export default interface ClassBlueprint * * @type {PropertyKey[]} */ - members: PropertyKey[]; + members?: PropertyKey[]; } \ No newline at end of file From a85f43f615f4b1ec6551364b4336c129c845e4e3 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 13:25:43 +0100 Subject: [PATCH 279/424] Add classLooksLike() util function --- .../support/src/reflections/classLooksLike.ts | 59 +++++++ packages/support/src/reflections/index.ts | 1 + .../reflections/classLooksLike.test.js | 160 ++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 packages/support/src/reflections/classLooksLike.ts create mode 100644 tests/browser/packages/support/reflections/classLooksLike.test.js diff --git a/packages/support/src/reflections/classLooksLike.ts b/packages/support/src/reflections/classLooksLike.ts new file mode 100644 index 00000000..e37c9df1 --- /dev/null +++ b/packages/support/src/reflections/classLooksLike.ts @@ -0,0 +1,59 @@ +import type { ClassBlueprint } from "@aedart/contracts/support/reflections"; +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { includesAll } from "@aedart/support/arrays"; +import { hasPrototypeProperty } from "./hasPrototypeProperty"; +import { classOwnKeys } from "./classOwnKeys"; + +/** + * Determine if target class look like given blueprint. + * + * @param {object} target + * @param {ClassBlueprint} blueprint + * + * @throws {TypeError} If target object does not have "prototype" property. Or, if blueprint does not contain at least + * one member or static member. + */ +export function classLooksLike(target: object, blueprint: ClassBlueprint): boolean +{ + if (!hasPrototypeProperty(target)) { + return false; + } + + // Abort if both blueprint does not define either members or static members property + const hasBlueprintStaticMembers: boolean = Reflect.has(blueprint, 'staticMembers'); + const hasBlueprintMembers: boolean = Reflect.has(blueprint, 'members'); + + if (!hasBlueprintStaticMembers && !hasBlueprintMembers) { + throw new TypeError('Blueprint must at least have a "members" or "staticMembers" property defined'); + } + + // Abort if both members and static members properties are empty + const amountStaticMembers = blueprint?.staticMembers?.length || 0; + const amountMembers = blueprint?.members?.length || 0; + + // Check for static members + let hasAllStaticMembers: boolean = false; + if (amountStaticMembers > 0) { + for (const staticMember of (blueprint.staticMembers as PropertyKey[])) { + if (!Reflect.has(target as object, staticMember)) { + return false; + } + } + + hasAllStaticMembers = true; + } + + // Check for members + if (amountMembers > 0) { + // We can return here, because static members have been checked and code aborted if a member + // was missing... + return includesAll( + classOwnKeys(target as ConstructorOrAbstractConstructor, true), + (blueprint.members as PropertyKey[]) + ); + } + + // Otherwise, if there were any static members and all a present in target, then + // check passes. + return amountStaticMembers > 0 && hasAllStaticMembers; +} \ No newline at end of file diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index 92a0b858..3d0ccce8 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -1,4 +1,5 @@ export * from './assertHasPrototypeProperty'; +export * from './classLooksLike'; export * from './classOwnKeys'; export * from './getAllParentsOfClass'; export * from './getClassPropertyDescriptor'; diff --git a/tests/browser/packages/support/reflections/classLooksLike.test.js b/tests/browser/packages/support/reflections/classLooksLike.test.js new file mode 100644 index 00000000..348436d3 --- /dev/null +++ b/tests/browser/packages/support/reflections/classLooksLike.test.js @@ -0,0 +1,160 @@ +import { classLooksLike } from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('classLooksLike()', () => { + + it('fails if blueprint has no members or static members property defined', () => { + class A {} + + const callback = () => { + classLooksLike(A, { }); + } + + expect(callback) + .toThrowError(TypeError); + }); + + it('does not fail when members property is defined in blueprint', () => { + class A {} + + const callback = () => { + classLooksLike(A, { members: [] }); + } + + expect(callback) + .not + .toThrowError(TypeError); + }); + + it('does not fail when static members property is defined in blueprint', () => { + class A {} + + const callback = () => { + classLooksLike(A, { staticMembers: [] }); + } + + expect(callback) + .not + .toThrowError(TypeError); + }); + + it('can determine if class looks like blueprint', () => { + + class A { + foo() {} + + bar() {} + + static sayHi() {} + } + + class B extends A { + + get zim() {} + + static goodBye() {} + } + + // --------------------------------------------------------------------------------------- // + + const data = [ + { + target: A, + blueprint: { + members: [ + 'foo', + 'bar', + 'zim' // does not exist in A + ] + }, + expected: false, + name: 'A (member that does not exist)' + }, + { + target: A, + blueprint: { + members: [ + 'bar', + 'foo', + ] + }, + expected: true, + name: 'A (all members that exist)' + }, + { + target: A, + blueprint: { + staticMembers: [ + 'sayHi', + 'goodBye' // does not exist in A + ], + members: [ + 'bar', + 'foo', + ] + }, + expected: false, + name: 'A (static member that does not exist)' + }, + { + target: B, + blueprint: { + members: [ + 'foo', // inherited + 'bar', // inherited + 'zim' + ] + }, + expected: true, + name: 'B (inherited members that exist)' + }, + { + target: B, + blueprint: { + staticMembers: [ + 'sayHi', // inherited + 'goodBye' + ], + members: [ + // Should just be ignored + ] + }, + expected: true, + name: 'B (inherited static members that exist)' + }, + { + target: B, + blueprint: { + staticMembers: [ + 'sayHi', // inherited + 'goodBye' + ], + members: [ + 'foo', // inherited + 'bar', // inherited + 'zim' + ] + }, + expected: true, + name: 'B (inherited all members that exist)' + }, + { + target: B, + blueprint: { + staticMembers: [], + members: [] + }, + expected: false, + name: 'B (empty blueprint)' + }, + ]; + + for (const entry of data) { + expect(classLooksLike(entry.target, entry.blueprint)) + .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) + .toBe(entry.expected); + } + }); + + }); +}); \ No newline at end of file From db231c1701c86e1d7235be637c3757f16483fcca Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 13:38:10 +0100 Subject: [PATCH 280/424] Add isSubclassOrLooksLike() util function --- packages/support/src/reflections/index.ts | 1 + .../src/reflections/isSubclassOrLooksLike.ts | 27 +++++++++ .../reflections/isSubclassOrLooksLike.test.js | 59 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 packages/support/src/reflections/isSubclassOrLooksLike.ts create mode 100644 tests/browser/packages/support/reflections/isSubclassOrLooksLike.test.js diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index 3d0ccce8..9a784b0b 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -16,4 +16,5 @@ export * from './isConstructor'; export * from './isKeySafe'; export * from './isKeyUnsafe'; export * from './isSubclass'; +export * from './isSubclassOrLooksLike'; export * from './isWeakKind'; \ No newline at end of file diff --git a/packages/support/src/reflections/isSubclassOrLooksLike.ts b/packages/support/src/reflections/isSubclassOrLooksLike.ts new file mode 100644 index 00000000..ef8d7e05 --- /dev/null +++ b/packages/support/src/reflections/isSubclassOrLooksLike.ts @@ -0,0 +1,27 @@ +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ClassBlueprint } from "@aedart/contracts/support/reflections"; +import { isSubclass } from "./isSubclass"; +import { classLooksLike } from "./classLooksLike"; + +/** + * Determine if target class is a subclass of given superclass, or if it looks like given blueprint + * + * **Note**: _Method is an alias for `isSubclass(target, superclass) || classLooksLike(target, blueprint)`._ + * + * @see isSubclass + * @see classLooksLike + * + * @param {object} target + * @param {ConstructorOrAbstractConstructor} superclass + * @param {ClassBlueprint} blueprint + * + * @throws {TypeError} + */ +export function isSubclassOrLooksLike( + target: object, + superclass: ConstructorOrAbstractConstructor, + blueprint: ClassBlueprint +): boolean +{ + return isSubclass(target, superclass) || classLooksLike(target, blueprint); +} \ No newline at end of file diff --git a/tests/browser/packages/support/reflections/isSubclassOrLooksLike.test.js b/tests/browser/packages/support/reflections/isSubclassOrLooksLike.test.js new file mode 100644 index 00000000..ae680833 --- /dev/null +++ b/tests/browser/packages/support/reflections/isSubclassOrLooksLike.test.js @@ -0,0 +1,59 @@ +import { isSubclassOrLooksLike } from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('isSubclassOrLooksLike()', () => { + + it('can determine if target is subclass or looks like blueprint', () => { + + class A { + foo() {} + } + class B extends A{} + + class C { + foo() {} + } + + // --------------------------------------------------------------------------------------- // + + const data = [ + { + target: B, + superclass: A, + blueprint: { + staticMembers: [], + members: [] + }, + expected: true, + name: 'B (should be superclass of A)' + }, + { + target: C, + superclass: A, + blueprint: { + staticMembers: [], + members: [ 'foo' ] + }, + expected: true, + name: 'C (should "look like" class A)' + }, + { + target: C, + superclass: B, + blueprint: { + staticMembers: [], + members: [ 'bar' ] + }, + expected: false, + name: 'C (should "look like" class B or contain member)' + }, + ]; + + for (const entry of data) { + expect(isSubclassOrLooksLike(entry.target, entry.superclass, entry.blueprint)) + .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) + .toBe(entry.expected); + } + }); + }); +}); \ No newline at end of file From 007a95bb320a10e2a76120b88024df277053a0c1 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 13:38:16 +0100 Subject: [PATCH 281/424] Change release notes --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0534a01..4fd36640 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `LogicalError` and `AbstractClassError` exceptions in `@aedart/support/exceptions`. * `getErrorMessage()`, `configureCustomError()` and `configureStackTrace()` in `@aedart/support/exceptions`. * `FUNCTION_PROTOTYPE` and `TYPED_ARRAY_PROTOTYPE` constants in `@aedart/contracts/support/reflections`. +* `ClassBlueprint` interface in `@aedart/contracts/support/reflections`. * `DANGEROUS_PROPERTIES` constant in `@aedart/contracts/support/objects`. * `hasPrototypeProperty()` and `assertHasPrototypeProperty()` in `@aedart/support/reflections`. * `getParentOfClass()` and `getAllParentsOfClass()` in `@aedart/support/reflections`. @@ -25,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `isWeakKind()` in `@aedart/support/reflections`. * `isKeySafe()` and `isKeyUnsafe()` in `@aedart/support/reflections`. * `getConstructorName()` and `getNameOrDesc()` in `@aedart/support/reflections`. -* `isSubclass()`, `classOwnKeys()`, `hasMethod()` and `hasAllMethods()` in `@aedart/support/reflections`. +* `isSubclass()`, `classLooksLike()`, `isSubclassOrLooksLike()`, `classOwnKeys()`, `hasMethod()` and `hasAllMethods()` in `@aedart/support/reflections`. * `merge()`, `populate()`, `isCloneable()` and `isPopulatable()` in `@aedart/support/objects`. * Objects `Merger` (_underlying component for the objects `merge()` util_) in `@aedart/support/objects`. * `merge()`, `isTypedArray()`, `isArrayLike()`, `isSafeArrayLike()`, `isTypedArray()` and `isConcatSpreadable()` in `@aedart/support/arrays`. From 5a6919d2598954eaec805d165fa575a48916e756 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 13:53:30 +0100 Subject: [PATCH 282/424] Refactor, use isSubclassOrLooksLike() util method This is so much easier to read. --- .../src/concerns/ConcernClassBlueprint.ts | 21 ++++++++++++ packages/support/src/concerns/index.ts | 2 ++ .../src/concerns/isConcernConstructor.ts | 32 +++---------------- 3 files changed, 27 insertions(+), 28 deletions(-) create mode 100644 packages/support/src/concerns/ConcernClassBlueprint.ts diff --git a/packages/support/src/concerns/ConcernClassBlueprint.ts b/packages/support/src/concerns/ConcernClassBlueprint.ts new file mode 100644 index 00000000..603aff95 --- /dev/null +++ b/packages/support/src/concerns/ConcernClassBlueprint.ts @@ -0,0 +1,21 @@ +import type { ClassBlueprint } from "@aedart/contracts/support/reflections"; +import { PROVIDES } from "@aedart/contracts/support/concerns"; + +/** + * Concern Class Blueprint + * + * Defines the minimum members that a target class should contain, before it is + * considered to "look like" a [Concern Class]{@link import('@aedart/contracts/support/concerns').ConcernConstructor} + * + * @see ClassBlueprint + */ +export const ConcernClassBlueprint: ClassBlueprint = { + staticMembers: [ + 'constructor', + PROVIDES + ], + + members: [ + 'concernOwner' + ] +}; \ No newline at end of file diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 5ca8dd90..771c4f2f 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -1,8 +1,10 @@ import AbstractConcern from "./AbstractConcern"; +import { ConcernClassBlueprint } from "./ConcernClassBlueprint"; import ConcernsContainer from "./ConcernsContainer"; import ConcernsInjector from "./ConcernsInjector"; export { + ConcernClassBlueprint, AbstractConcern, ConcernsContainer, ConcernsInjector diff --git a/packages/support/src/concerns/isConcernConstructor.ts b/packages/support/src/concerns/isConcernConstructor.ts index 34b28566..0def1244 100644 --- a/packages/support/src/concerns/isConcernConstructor.ts +++ b/packages/support/src/concerns/isConcernConstructor.ts @@ -1,8 +1,7 @@ -import {isSubclass, classOwnKeys} from "@aedart/support/reflections"; -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; -import { PROVIDES } from "@aedart/contracts/support/concerns"; +import { isSubclassOrLooksLike} from "@aedart/support/reflections"; import { isset } from "@aedart/support/misc"; import AbstractConcern from "./AbstractConcern"; +import { ConcernClassBlueprint } from "./ConcernClassBlueprint"; /** * In-memory cache of classes that are determined to be of the type @@ -34,33 +33,10 @@ export function isConcernConstructor( return true; } - // Easiest way to determine is be checking if target is a subclass of Abstract Concern - if (isSubclass(target, AbstractConcern)) { + if (isSubclassOrLooksLike(target, AbstractConcern, ConcernClassBlueprint)) { concernConstructorsCache.add(target); return true; } - // However, if given target is a custom implementation, then there are a few members - // that MUST be available, before it can be considered a concern constructor - const staticMembers: PropertyKey[] = [ PROVIDES ]; - const members: PropertyKey[] = [ 'concernOwner' ]; - - // Abort if static members are not available... - for (const staticMember of staticMembers) { - if (!Reflect.has(target as object, staticMember)) { - return false; - } - } - - // Abort if members are not available... - const classKeys: PropertyKey[] = classOwnKeys(target as ConstructorOrAbstractConstructor, true); - for (const member of members) { - if (!classKeys.includes(member)) { - return false; - } - } - - // Thus, if all required members are available then the given target is a valid concern class. - concernConstructorsCache.add(target); - return true; + return false; } \ No newline at end of file From 9b2c08254f4e07b24e7548d0c56b0b2a8b68513b Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 14:27:33 +0100 Subject: [PATCH 283/424] Cleanup --- packages/support/src/misc/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/misc/index.ts b/packages/support/src/misc/index.ts index 71688fc5..761965f2 100644 --- a/packages/support/src/misc/index.ts +++ b/packages/support/src/misc/index.ts @@ -1,8 +1,8 @@ +export * from './descTag'; export * from './empty'; export * from './isKey'; export * from './isPrimitive'; export * from './isPropertyKey'; export * from './isset'; -export * from './descTag'; export * from './mergeKeys'; export * from './toWeakRef'; From dc538f1f58d2a634ef6d536e9cf4cf3d7af46ac3 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 14:31:15 +0100 Subject: [PATCH 284/424] Cleanup internal types --- packages/support/src/misc/mergeKeys.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/support/src/misc/mergeKeys.ts b/packages/support/src/misc/mergeKeys.ts index 80580e8b..500c209c 100644 --- a/packages/support/src/misc/mergeKeys.ts +++ b/packages/support/src/misc/mergeKeys.ts @@ -17,17 +17,20 @@ export function mergeKeys(...keys: Key[]): Key return []; } - keys = keys.map((key: Key, index: number) => { - if (!isKey(key)) { + const mapped = keys.map((key: Key, index: number) => { + let modifiedKey = key; + + if (!isKey(modifiedKey)) { throw new TypeError(`mergeKeys(): Argument #${index} must be a valid "key", ${typeof key} given`); } - if (!Array.isArray(key)) { - key = [ key ]; + if (!Array.isArray(modifiedKey)) { + modifiedKey = [ modifiedKey ] as Key; } - return key; + return modifiedKey; }); - return [].concat(...keys); + // @ts-expect-error This should be fine here. TS does not understand this merge... + return [].concat(...mapped) as Key; } \ No newline at end of file From 6c0b775dca112efad26a1636f617ffff71ad0029 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 14:46:53 +0100 Subject: [PATCH 285/424] Add additional cases --- tests/browser/packages/support/arrays/isTypedArray.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/browser/packages/support/arrays/isTypedArray.test.js b/tests/browser/packages/support/arrays/isTypedArray.test.js index 562ae166..bca5ed04 100644 --- a/tests/browser/packages/support/arrays/isTypedArray.test.js +++ b/tests/browser/packages/support/arrays/isTypedArray.test.js @@ -5,7 +5,12 @@ describe('@aedart/support/arrays', () => { it('can determine if object is Typed Array', () => { const dataSet = [ + { value: undefined, expected: false, name: 'Undefined' }, + { value: null, expected: false, name: 'Null' }, { value: [], expected: false, name: 'Array' }, + { value: {}, expected: false, name: 'Object (empty)' }, + { value: 123, expected: false, name: 'Number' }, + { value: 'foo', expected: false, name: 'String' }, { value: new Map(), expected: false, name: 'Map' }, { value: new Int8Array(), expected: true, name: 'Int8Array' }, From 8710b1951544142007cee028c2f93dd6a42206d5 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 15:04:25 +0100 Subject: [PATCH 286/424] Improve description of CONCERN_CLASSES const --- packages/contracts/src/support/concerns/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index fe53e989..ca260787 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -56,8 +56,7 @@ export const HIDDEN: unique symbol = Symbol('hidden'); export const PROVIDES : unique symbol = Symbol('concern_provides'); /** - * Symbol used to define a list of the concern classes that a given target class - * must use. + * Symbol used to define a list of the concern classes to be used by a target class. * * @see {MustUseConcerns} * From b97ad1f9c8aa746c65e71c63e7cb668a5042545f Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 15:07:53 +0100 Subject: [PATCH 287/424] Simplify the ConcernClasses type alias Also, improved desc. of [CONCERN_CLASSES]() static method. --- .../src/support/concerns/MustUseConcerns.ts | 8 +++++--- packages/contracts/src/support/concerns/types.ts | 15 ++------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/contracts/src/support/concerns/MustUseConcerns.ts b/packages/contracts/src/support/concerns/MustUseConcerns.ts index 5d4c502a..3178b9ff 100644 --- a/packages/contracts/src/support/concerns/MustUseConcerns.ts +++ b/packages/contracts/src/support/concerns/MustUseConcerns.ts @@ -28,12 +28,14 @@ export default interface MustUseConcerns ): ConstructorOrAbstractConstructor; /** - * Returns the concern classes that this class must use. + * Returns the concern classes that must be used by this target class / Concern Owner. * * **Note**: _If this class' parent class also must use concern classes, - * then those concern classes are included in the resulting list, ordered first!_ + * then those concern classes are included in the resulting list._ * + * @static + * * @return {ConcernClasses} */ - [CONCERN_CLASSES](): ConcernClasses; + [CONCERN_CLASSES](): ConcernClasses; } \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/types.ts b/packages/contracts/src/support/concerns/types.ts index 32309490..b5a8d0c4 100644 --- a/packages/contracts/src/support/concerns/types.ts +++ b/packages/contracts/src/support/concerns/types.ts @@ -20,17 +20,6 @@ export type Aliases = { }; /** - * Array that holds a {@link Concern} class / Owner class pair. + * A list of concern classes and their owner class in which they are used. */ -export type ConcernOwnerClassPair = [ - ConcernConstructor, // Concern Class - ConstructorOrAbstractConstructor // Owner class that must use the concern class -]; - -/** - * A list of concern classes and their owner class in which they are - * used. - * - * @see ConcernOwnerClassPair - */ -export type ConcernClasses = ConcernOwnerClassPair[]; \ No newline at end of file +export type ConcernClasses = Map \ No newline at end of file From 2f0e3f3d4bf358b85bacbe9da7c2a3067c12ca1b Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 15:35:34 +0100 Subject: [PATCH 288/424] Add ConcernsMap interface --- .../src/support/concerns/ConcernsMap.ts | 77 +++++++++++++++++++ .../contracts/src/support/concerns/index.ts | 2 + 2 files changed, 79 insertions(+) create mode 100644 packages/contracts/src/support/concerns/ConcernsMap.ts diff --git a/packages/contracts/src/support/concerns/ConcernsMap.ts b/packages/contracts/src/support/concerns/ConcernsMap.ts new file mode 100644 index 00000000..63d94a63 --- /dev/null +++ b/packages/contracts/src/support/concerns/ConcernsMap.ts @@ -0,0 +1,77 @@ +import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import ConcernConstructor from "./ConcernConstructor"; + +/** + * Concern Classes Map + */ +export default interface ConcernsMap +{ + /** + * The amount of concern classes in this map + * + * @readonly + * + * @type {number} + */ + readonly size: number; + + /** + * Determine if concern class is registered in this map + * + * @param {ConcernConstructor} concern + */ + has(concern: ConcernConstructor): boolean; + + /** + * Determine if this map is empty + * + * @return {boolean} + */ + isEmpty(): boolean; + + /** + * Opposite of {@link isEmpty} + * + * @return {boolean} + */ + isNotEmpty(): boolean; + + /** + * Returns all concern constructor - target class pairs in this map + * + * @return {IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]>} + */ + [Symbol.iterator](): IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]>; + + /** + * Returns all concern constructor - target class pairs in this map + * + * @return {IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]>} + */ + all(): IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]>; + + /** + * Returns all concern classes registered in this map + * + * @return {IterableIterator} + */ + concerns(): IterableIterator; + + /** + * Returns all target classes registered in this map + * + * @return {IterableIterator} + */ + classes(): IterableIterator; + + /** + * Merge this map with another concerns classes map + * + * @param {ConcernsMap} map + * + * @return {ConcernsMap} New Concern Classes Map instance + * + * @throws {InjectionException} If entries from given map already exist in this map. + */ + merge(map: ConcernsMap): ConcernsMap; +} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index ca260787..15b71f8f 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -122,6 +122,7 @@ import Configuration from "./Configuration"; import Container from "./Container"; import MustUseConcerns from "./MustUseConcerns"; import Injector from "./Injector"; +import ConcernsMap from "./ConcernsMap"; import Owner from "./Owner"; export { type Concern, @@ -130,6 +131,7 @@ export { type Container, type MustUseConcerns, type Injector, + type ConcernsMap, type Owner } From 86d0f76be26f4f208e3bc99d8f3fc74308fa65f8 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 15:40:51 +0100 Subject: [PATCH 289/424] Change MustUseConcerns interface, use new ConcernsMap The [CONCERN_CLASSES] is now a static property that must hold a Concerns Map instance. This should make it easier to work with. --- .../contracts/src/support/concerns/MustUseConcerns.ts | 10 +++++----- packages/contracts/src/support/concerns/types.ts | 9 +-------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/contracts/src/support/concerns/MustUseConcerns.ts b/packages/contracts/src/support/concerns/MustUseConcerns.ts index 3178b9ff..7909969b 100644 --- a/packages/contracts/src/support/concerns/MustUseConcerns.ts +++ b/packages/contracts/src/support/concerns/MustUseConcerns.ts @@ -1,6 +1,6 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; -import type { ConcernClasses, Concern } from "./index"; import { CONCERN_CLASSES } from "./index"; +import ConcernsMap from "./ConcernsMap"; import Owner from "./Owner"; /** @@ -28,14 +28,14 @@ export default interface MustUseConcerns ): ConstructorOrAbstractConstructor; /** - * Returns the concern classes that must be used by this target class / Concern Owner. + * A map of the concern classes to be used by this target class / Concern Owner instance. * * **Note**: _If this class' parent class also must use concern classes, - * then those concern classes are included in the resulting list._ + * then those concern classes are included in the resulting concerns map._ * * @static * - * @return {ConcernClasses} + * @type {ConcernsMap} */ - [CONCERN_CLASSES](): ConcernClasses; + [CONCERN_CLASSES]: ConcernsMap; } \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/types.ts b/packages/contracts/src/support/concerns/types.ts index b5a8d0c4..004cf0f5 100644 --- a/packages/contracts/src/support/concerns/types.ts +++ b/packages/contracts/src/support/concerns/types.ts @@ -1,6 +1,4 @@ -import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import Concern from "./Concern"; -import ConcernConstructor from "./ConcernConstructor"; /** * An alias for a property or method in a {@link Concern} class @@ -17,9 +15,4 @@ export type Alias = PropertyKey; */ export type Aliases = { [K in keyof T]: Alias -}; - -/** - * A list of concern classes and their owner class in which they are used. - */ -export type ConcernClasses = Map \ No newline at end of file +}; \ No newline at end of file From d9c7a5fb6dc844003cf1eb26cd74f7074faaf7ff Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 15:51:50 +0100 Subject: [PATCH 290/424] Add interface for Concerns Map Constructor --- .../concerns/ConcernsMapConstructor.ts | 20 +++++++++++++++++++ .../contracts/src/support/concerns/index.ts | 10 ++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 packages/contracts/src/support/concerns/ConcernsMapConstructor.ts diff --git a/packages/contracts/src/support/concerns/ConcernsMapConstructor.ts b/packages/contracts/src/support/concerns/ConcernsMapConstructor.ts new file mode 100644 index 00000000..eb031ad2 --- /dev/null +++ b/packages/contracts/src/support/concerns/ConcernsMapConstructor.ts @@ -0,0 +1,20 @@ +import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import ConcernsMap from './ConcernsMap'; +import ConcernConstructor from "./ConcernConstructor"; + +/** + * Concerns Classes Map Constructor + * + * @see ConcernsMap + */ +export default interface ConcernsMapConstructor +{ + /** + * Create a new Concern Classes Map instance + * + * @param {[ConcernConstructor, ConstructorOrAbstractConstructor][]} entries Key-value pair + * + * @throws {InjectionException} If an already added concern constructor is attempted added again. + */ + new (entries: [ConcernConstructor, ConstructorOrAbstractConstructor][]): ConcernsMap; +} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 15b71f8f..ceaf77d4 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -42,7 +42,7 @@ export const HIDDEN: unique symbol = Symbol('hidden'); * ```ts * class MyConcern implements Concern * { - * static [PROPERTIES](): PropertyKey[] + * static [PROVIDES](): PropertyKey[] * { * // ...not shown... * } @@ -56,7 +56,7 @@ export const HIDDEN: unique symbol = Symbol('hidden'); export const PROVIDES : unique symbol = Symbol('concern_provides'); /** - * Symbol used to define a list of the concern classes to be used by a target class. + * Symbol used to define a map of the concern classes to be used by a target class. * * @see {MustUseConcerns} * @@ -123,15 +123,17 @@ import Container from "./Container"; import MustUseConcerns from "./MustUseConcerns"; import Injector from "./Injector"; import ConcernsMap from "./ConcernsMap"; +import ConcernsMapConstructor from "./ConcernsMapConstructor"; import Owner from "./Owner"; export { type Concern, type ConcernConstructor, + type ConcernsMap, + type ConcernsMapConstructor, type Configuration, type Container, - type MustUseConcerns, type Injector, - type ConcernsMap, + type MustUseConcerns, type Owner } From 401ac4affd3337012d925999cad8662602d89ee2 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 16:25:10 +0100 Subject: [PATCH 291/424] Add Concern Classes Map (incomplete) --- .../support/src/concerns/ConcernClassesMap.ts | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 packages/support/src/concerns/ConcernClassesMap.ts diff --git a/packages/support/src/concerns/ConcernClassesMap.ts b/packages/support/src/concerns/ConcernClassesMap.ts new file mode 100644 index 00000000..ac7bf4c7 --- /dev/null +++ b/packages/support/src/concerns/ConcernClassesMap.ts @@ -0,0 +1,160 @@ +import type {ConcernConstructor, ConcernsMap, ConcernsMapConstructor} from "@aedart/contracts/support/concerns"; +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts/types"; +import {InjectionError} from "@aedart/support/concerns/exceptions"; + +/** + * Concern Classes Map + * + * @see ConcernsMap + */ +export default class ConcernClassesMap implements ConcernsMap { + + /** + * Map of the concern classes and the target classes that must use them + * + * @private + * @readonly + * + * @type {Map} + */ + readonly #map: Map; + + /** + * Create a new Concern Classes Map instance + * + * @param {[ConcernConstructor, ConstructorOrAbstractConstructor][]} entries Key-value pair + * + * @throws {InjectionException} If an already added concern constructor is attempted added again. + */ + public constructor(entries: [ConcernConstructor, ConstructorOrAbstractConstructor][]) + { + this.#map = new Map(); + + this.#addEntries(entries); + } + + /** + * The amount of concern classes in this map + * + * @readonly + * + * @type {number} + */ + public get size(): number + { + return this.#map.size; + } + + /** + * Determine if concern class is registered in this map + * + * @param {ConcernConstructor} concern + */ + public has(concern: ConcernConstructor): boolean + { + return this.#map.has(concern); + } + + /** + * Determine if this map is empty + * + * @return {boolean} + */ + public isEmpty(): boolean + { + return this.size === 0; + } + + /** + * Opposite of {@link isEmpty} + * + * @return {boolean} + */ + public isNotEmpty(): boolean + { + return !this.isEmpty(); + } + + /** + * Returns all concern constructor - target class pairs in this map + * + * @return {IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]>} + */ + [Symbol.iterator](): IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]> + { + return this.all(); + } + + /** + * Returns all concern constructor - target class pairs in this map + * + * @return {IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]>} + */ + public all(): IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]> + { + return this.#map.entries(); + } + + /** + * Returns all concern classes registered in this map + * + * @return {IterableIterator} + */ + public concerns(): IterableIterator + { + return this.#map.keys(); + } + + /** + * Returns all target classes registered in this map + * + * @return {IterableIterator} + */ + public classes(): IterableIterator + { + return this.#map.values(); + } + + /** + * Merge this map with another concerns classes map + * + * @param {ConcernsMap} map + * + * @return {ConcernsMap} New Concern Classes Map instance + * + * @throws {InjectionException} If entries from given map already exist in this map. + */ + public merge(map: ConcernsMap): ConcernsMap + { + const entries = [ + ...Array.from(this.all()), + ...Array.from(map.all()) + ]; + + return new (this.constructor as ConcernsMapConstructor)(entries); + } + + // TODO: + #addEntries(entries: [ConcernConstructor, ConstructorOrAbstractConstructor][]): this + { + for (const entry of entries) { + this.#addEntry(entry[0], entry[1]); + } + + return this; + } + + // TODO: + #addEntry(concern: ConcernConstructor, target: ConstructorOrAbstractConstructor): this + { + if (this.has(concern)) { + // TODO: FAIL here... + // const existingTarget = this.map.get(concern); + // throw new InjectionError() + } + + this.#map.set(concern, target); + + return this; + } +} \ No newline at end of file From 180c81146734d9c84f15f160786c28cddad82c71 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 23 Feb 2024 16:25:18 +0100 Subject: [PATCH 292/424] Export Concern Classes Map --- packages/support/src/concerns/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 771c4f2f..b54d8f75 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -1,11 +1,13 @@ import AbstractConcern from "./AbstractConcern"; import { ConcernClassBlueprint } from "./ConcernClassBlueprint"; +import ConcernClassesMap from "./ConcernClassesMap"; import ConcernsContainer from "./ConcernsContainer"; import ConcernsInjector from "./ConcernsInjector"; export { - ConcernClassBlueprint, AbstractConcern, + ConcernClassBlueprint, + ConcernClassesMap, ConcernsContainer, ConcernsInjector }; From a7ca7efeaf72c31a57364a58c1fc84a7ea44374e Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 19:07:57 +0100 Subject: [PATCH 293/424] Remove Concern Classes Map component The main idea was to gain an overview of what parent class registered what concern class. However, this only yields a value during registration if there is a conflict. Thus, this can be replaced with a simple static array and allow the Injector to handle the rest. --- .../src/support/concerns/ConcernsMap.ts | 77 --------- .../concerns/ConcernsMapConstructor.ts | 20 --- .../src/support/concerns/MustUseConcerns.ts | 13 +- .../contracts/src/support/concerns/index.ts | 4 - .../support/src/concerns/ConcernClassesMap.ts | 160 ------------------ packages/support/src/concerns/index.ts | 2 - 6 files changed, 7 insertions(+), 269 deletions(-) delete mode 100644 packages/contracts/src/support/concerns/ConcernsMap.ts delete mode 100644 packages/contracts/src/support/concerns/ConcernsMapConstructor.ts delete mode 100644 packages/support/src/concerns/ConcernClassesMap.ts diff --git a/packages/contracts/src/support/concerns/ConcernsMap.ts b/packages/contracts/src/support/concerns/ConcernsMap.ts deleted file mode 100644 index 63d94a63..00000000 --- a/packages/contracts/src/support/concerns/ConcernsMap.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; -import ConcernConstructor from "./ConcernConstructor"; - -/** - * Concern Classes Map - */ -export default interface ConcernsMap -{ - /** - * The amount of concern classes in this map - * - * @readonly - * - * @type {number} - */ - readonly size: number; - - /** - * Determine if concern class is registered in this map - * - * @param {ConcernConstructor} concern - */ - has(concern: ConcernConstructor): boolean; - - /** - * Determine if this map is empty - * - * @return {boolean} - */ - isEmpty(): boolean; - - /** - * Opposite of {@link isEmpty} - * - * @return {boolean} - */ - isNotEmpty(): boolean; - - /** - * Returns all concern constructor - target class pairs in this map - * - * @return {IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]>} - */ - [Symbol.iterator](): IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]>; - - /** - * Returns all concern constructor - target class pairs in this map - * - * @return {IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]>} - */ - all(): IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]>; - - /** - * Returns all concern classes registered in this map - * - * @return {IterableIterator} - */ - concerns(): IterableIterator; - - /** - * Returns all target classes registered in this map - * - * @return {IterableIterator} - */ - classes(): IterableIterator; - - /** - * Merge this map with another concerns classes map - * - * @param {ConcernsMap} map - * - * @return {ConcernsMap} New Concern Classes Map instance - * - * @throws {InjectionException} If entries from given map already exist in this map. - */ - merge(map: ConcernsMap): ConcernsMap; -} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/ConcernsMapConstructor.ts b/packages/contracts/src/support/concerns/ConcernsMapConstructor.ts deleted file mode 100644 index eb031ad2..00000000 --- a/packages/contracts/src/support/concerns/ConcernsMapConstructor.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; -import ConcernsMap from './ConcernsMap'; -import ConcernConstructor from "./ConcernConstructor"; - -/** - * Concerns Classes Map Constructor - * - * @see ConcernsMap - */ -export default interface ConcernsMapConstructor -{ - /** - * Create a new Concern Classes Map instance - * - * @param {[ConcernConstructor, ConstructorOrAbstractConstructor][]} entries Key-value pair - * - * @throws {InjectionException} If an already added concern constructor is attempted added again. - */ - new (entries: [ConcernConstructor, ConstructorOrAbstractConstructor][]): ConcernsMap; -} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/MustUseConcerns.ts b/packages/contracts/src/support/concerns/MustUseConcerns.ts index 7909969b..c740e02b 100644 --- a/packages/contracts/src/support/concerns/MustUseConcerns.ts +++ b/packages/contracts/src/support/concerns/MustUseConcerns.ts @@ -1,6 +1,6 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import { CONCERN_CLASSES } from "./index"; -import ConcernsMap from "./ConcernsMap"; +import ConcernConstructor from "./ConcernConstructor"; import Owner from "./Owner"; /** @@ -28,14 +28,15 @@ export default interface MustUseConcerns ): ConstructorOrAbstractConstructor; /** - * A map of the concern classes to be used by this target class / Concern Owner instance. + * A list of the concern classes to be used by this target class. * - * **Note**: _If this class' parent class also must use concern classes, - * then those concern classes are included in the resulting concerns map._ + * **Note**: _If this class' parent class also uses concerns, then those + * concern classes are included recursively in this list, in the order + * that they have been registered._ * * @static * - * @type {ConcernsMap} + * @type {ConcernConstructor} */ - [CONCERN_CLASSES]: ConcernsMap; + [CONCERN_CLASSES]: ConcernConstructor[]; } \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index ceaf77d4..50beb353 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -122,14 +122,10 @@ import Configuration from "./Configuration"; import Container from "./Container"; import MustUseConcerns from "./MustUseConcerns"; import Injector from "./Injector"; -import ConcernsMap from "./ConcernsMap"; -import ConcernsMapConstructor from "./ConcernsMapConstructor"; import Owner from "./Owner"; export { type Concern, type ConcernConstructor, - type ConcernsMap, - type ConcernsMapConstructor, type Configuration, type Container, type Injector, diff --git a/packages/support/src/concerns/ConcernClassesMap.ts b/packages/support/src/concerns/ConcernClassesMap.ts deleted file mode 100644 index ac7bf4c7..00000000 --- a/packages/support/src/concerns/ConcernClassesMap.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type {ConcernConstructor, ConcernsMap, ConcernsMapConstructor} from "@aedart/contracts/support/concerns"; -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts/types"; -import {InjectionError} from "@aedart/support/concerns/exceptions"; - -/** - * Concern Classes Map - * - * @see ConcernsMap - */ -export default class ConcernClassesMap implements ConcernsMap { - - /** - * Map of the concern classes and the target classes that must use them - * - * @private - * @readonly - * - * @type {Map} - */ - readonly #map: Map; - - /** - * Create a new Concern Classes Map instance - * - * @param {[ConcernConstructor, ConstructorOrAbstractConstructor][]} entries Key-value pair - * - * @throws {InjectionException} If an already added concern constructor is attempted added again. - */ - public constructor(entries: [ConcernConstructor, ConstructorOrAbstractConstructor][]) - { - this.#map = new Map(); - - this.#addEntries(entries); - } - - /** - * The amount of concern classes in this map - * - * @readonly - * - * @type {number} - */ - public get size(): number - { - return this.#map.size; - } - - /** - * Determine if concern class is registered in this map - * - * @param {ConcernConstructor} concern - */ - public has(concern: ConcernConstructor): boolean - { - return this.#map.has(concern); - } - - /** - * Determine if this map is empty - * - * @return {boolean} - */ - public isEmpty(): boolean - { - return this.size === 0; - } - - /** - * Opposite of {@link isEmpty} - * - * @return {boolean} - */ - public isNotEmpty(): boolean - { - return !this.isEmpty(); - } - - /** - * Returns all concern constructor - target class pairs in this map - * - * @return {IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]>} - */ - [Symbol.iterator](): IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]> - { - return this.all(); - } - - /** - * Returns all concern constructor - target class pairs in this map - * - * @return {IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]>} - */ - public all(): IterableIterator<[ConcernConstructor, ConstructorOrAbstractConstructor]> - { - return this.#map.entries(); - } - - /** - * Returns all concern classes registered in this map - * - * @return {IterableIterator} - */ - public concerns(): IterableIterator - { - return this.#map.keys(); - } - - /** - * Returns all target classes registered in this map - * - * @return {IterableIterator} - */ - public classes(): IterableIterator - { - return this.#map.values(); - } - - /** - * Merge this map with another concerns classes map - * - * @param {ConcernsMap} map - * - * @return {ConcernsMap} New Concern Classes Map instance - * - * @throws {InjectionException} If entries from given map already exist in this map. - */ - public merge(map: ConcernsMap): ConcernsMap - { - const entries = [ - ...Array.from(this.all()), - ...Array.from(map.all()) - ]; - - return new (this.constructor as ConcernsMapConstructor)(entries); - } - - // TODO: - #addEntries(entries: [ConcernConstructor, ConstructorOrAbstractConstructor][]): this - { - for (const entry of entries) { - this.#addEntry(entry[0], entry[1]); - } - - return this; - } - - // TODO: - #addEntry(concern: ConcernConstructor, target: ConstructorOrAbstractConstructor): this - { - if (this.has(concern)) { - // TODO: FAIL here... - // const existingTarget = this.map.get(concern); - // throw new InjectionError() - } - - this.#map.set(concern, target); - - return this; - } -} \ No newline at end of file diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index b54d8f75..c03200ba 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -1,13 +1,11 @@ import AbstractConcern from "./AbstractConcern"; import { ConcernClassBlueprint } from "./ConcernClassBlueprint"; -import ConcernClassesMap from "./ConcernClassesMap"; import ConcernsContainer from "./ConcernsContainer"; import ConcernsInjector from "./ConcernsInjector"; export { AbstractConcern, ConcernClassBlueprint, - ConcernClassesMap, ConcernsContainer, ConcernsInjector }; From 0377f507d2a920fbb511595c26ca084cf6c4d7ef Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 19:21:08 +0100 Subject: [PATCH 294/424] Add Already Registered Exception --- .../exceptions/AlreadyRegisteredException.ts | 22 +++++++ .../src/support/concerns/exceptions/index.ts | 2 + .../exceptions/AlreadyRegisteredError.ts | 60 +++++++++++++++++++ .../support/src/concerns/exceptions/index.ts | 2 + 4 files changed, 86 insertions(+) create mode 100644 packages/contracts/src/support/concerns/exceptions/AlreadyRegisteredException.ts create mode 100644 packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts diff --git a/packages/contracts/src/support/concerns/exceptions/AlreadyRegisteredException.ts b/packages/contracts/src/support/concerns/exceptions/AlreadyRegisteredException.ts new file mode 100644 index 00000000..0b75090e --- /dev/null +++ b/packages/contracts/src/support/concerns/exceptions/AlreadyRegisteredException.ts @@ -0,0 +1,22 @@ +import { InjectionException } from "@aedart/contracts/support/concerns"; +import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import MustUseConcerns from "../MustUseConcerns"; + +/** + * Already Registered Exception + * + * To be thrown when a concern class is attempted registered in a target, but + * was previously registered in the target or target's parent. + */ +export default interface AlreadyRegisteredException extends InjectionException +{ + /** + * The source, e.g. a parent class, in which a concern class + * was already registered. + * + * @readonly + * + * @type {ConstructorOrAbstractConstructor|MustUseConcerns} + */ + readonly source: ConstructorOrAbstractConstructor | MustUseConcerns; +} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/exceptions/index.ts b/packages/contracts/src/support/concerns/exceptions/index.ts index 68cede44..13e3bc4a 100644 --- a/packages/contracts/src/support/concerns/exceptions/index.ts +++ b/packages/contracts/src/support/concerns/exceptions/index.ts @@ -1,8 +1,10 @@ +import AlreadyRegisteredException from "./AlreadyRegisteredException"; import BootException from "./BootException"; import ConcernException from "./ConcernException"; import InjectionException from "./InjectionException"; import NotRegisteredException from "./NotRegisteredException"; export { + type AlreadyRegisteredException, type BootException, type ConcernException, type InjectionException, diff --git a/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts b/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts new file mode 100644 index 00000000..57bf9fbe --- /dev/null +++ b/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts @@ -0,0 +1,60 @@ +import type { + AlreadyRegisteredException, + ConcernConstructor, + MustUseConcerns +} from "@aedart/contracts/support/concerns"; +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { configureCustomError } from "@aedart/support/exceptions"; +import { getNameOrDesc } from "@aedart/support/reflections"; +import { InjectionError } from "@aedart/support/concerns"; + +/** + * Already Registered Error + * + * @see AlreadyRegisteredException + */ +export default class AlreadyRegisteredError extends InjectionError implements AlreadyRegisteredException +{ + /** + * The source, e.g. a parent class, in which a concern class + * was already registered. + * + * @readonly + * @private + * + * @type {ConstructorOrAbstractConstructor|MustUseConcerns} + */ + readonly #source: ConstructorOrAbstractConstructor | MustUseConcerns; + + constructor( + target: ConstructorOrAbstractConstructor | MustUseConcerns, + concern: ConcernConstructor, + source: ConstructorOrAbstractConstructor | MustUseConcerns, + message?: string, + options?: ErrorOptions + ) { + const resolved = message || `Concern ${getNameOrDesc(concern)} is already registered in class ${getNameOrDesc(target)} (via class ${getNameOrDesc(target)})`; + + super(target, concern, resolved, options); + + configureCustomError(this); + + this.#source = source; + + // Force set the source in the cause + (this.cause as Record).source = source; + } + + /** + * The source, e.g. a parent class, in which a concern class + * was already registered. + * + * @readonly + * + * @returns {ConstructorOrAbstractConstructor | MustUseConcerns} + */ + get source(): ConstructorOrAbstractConstructor | MustUseConcerns + { + return this.#source; + } +} \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/index.ts b/packages/support/src/concerns/exceptions/index.ts index 78922f27..8de018b8 100644 --- a/packages/support/src/concerns/exceptions/index.ts +++ b/packages/support/src/concerns/exceptions/index.ts @@ -1,8 +1,10 @@ +import AlreadyRegisteredError from "./AlreadyRegisteredError"; import BootError from "./BootError"; import ConcernError from "./ConcernError"; import InjectionError from "./InjectionError"; import NotRegisteredError from "./NotRegisteredError"; export { + AlreadyRegisteredError, BootError, ConcernError, InjectionError, From ce2340ea9c4033e820b6c368bf72ef3ca7caec47 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 19:24:25 +0100 Subject: [PATCH 295/424] Allow defineConcerns() to throw "Already Registered" exception --- packages/contracts/src/support/concerns/Injector.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index cdb2076c..86ba0830 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -48,7 +48,7 @@ export default interface Injector * Defines the concern classes that must be used by the target class. * * **Note**: _Method changes the target class, such that it implements and respects the - * {@link MustUseConcerns} interface. The original target class' constructor remains the untouched!_ + * {@link MustUseConcerns} interface._ * * @template C extends Concern * @template T = object @@ -58,8 +58,9 @@ export default interface Injector * * @returns {MustUseConcerns} The modified target class * - * @throws {InjectionException} If given concern classes conflict with target class' parent concern classes, - * e.g. in case of duplicates. Or, if unable to modify target class. + * @throws {AlreadyRegisteredException} If given concern classes conflict with target class' parent concern classes, + * e.g. in case of duplicates. + * @throws {InjectionException} If unable to register concern classes in target class */ defineConcerns(target: T, concerns: ConcernConstructor[]): MustUseConcerns; From 5e909f2ef275b1366114fc4d2c9d3e92f5914b08 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 19:27:59 +0100 Subject: [PATCH 296/424] Fix description of CONCERN_CLASSES const --- packages/contracts/src/support/concerns/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 50beb353..82b310cd 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -56,7 +56,7 @@ export const HIDDEN: unique symbol = Symbol('hidden'); export const PROVIDES : unique symbol = Symbol('concern_provides'); /** - * Symbol used to define a map of the concern classes to be used by a target class. + * Symbol used to define a list of the concern classes to be used by a target class. * * @see {MustUseConcerns} * From 709f127a9858966b1fe9af4e5bc87ba7a1cd7892 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 19:54:40 +0100 Subject: [PATCH 297/424] Change exceptions, allow null as concern In some situations, a concern might not be the cause of an exception. --- .../support/concerns/exceptions/ConcernException.ts | 4 ++-- .../support/src/concerns/exceptions/ConcernError.ts | 12 ++++++------ .../src/concerns/exceptions/InjectionError.ts | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/contracts/src/support/concerns/exceptions/ConcernException.ts b/packages/contracts/src/support/concerns/exceptions/ConcernException.ts index 13f57a05..2a58412e 100644 --- a/packages/contracts/src/support/concerns/exceptions/ConcernException.ts +++ b/packages/contracts/src/support/concerns/exceptions/ConcernException.ts @@ -13,7 +13,7 @@ export default interface ConcernException extends Throwable { * * @readonly * - * @type {ConcernConstructor} + * @type {ConcernConstructor | null} */ - readonly concern: ConcernConstructor + readonly concern: ConcernConstructor | null; } \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/ConcernError.ts b/packages/support/src/concerns/exceptions/ConcernError.ts index 1a5d863b..0d69042a 100644 --- a/packages/support/src/concerns/exceptions/ConcernError.ts +++ b/packages/support/src/concerns/exceptions/ConcernError.ts @@ -13,18 +13,18 @@ export default class ConcernError extends Error implements ConcernException * * @private * - * @type {ConcernConstructor} + * @type {ConcernConstructor | null} */ - readonly #concern: ConcernConstructor + readonly #concern: ConcernConstructor | null /** * Create a new Concern Error instance * - * @param {ConcernConstructor} concern + * @param {ConcernConstructor | null} concern * @param {string} message * @param {ErrorOptions} [options] */ - constructor(concern: ConcernConstructor, message: string, options?: ErrorOptions) + constructor(concern: ConcernConstructor | null, message: string, options?: ErrorOptions) { super(message, options || { cause: {} }); @@ -41,9 +41,9 @@ export default class ConcernError extends Error implements ConcernException * * @readonly * - * @type {ConcernConstructor} + * @type {ConcernConstructor | null} */ - get concern(): ConcernConstructor + get concern(): ConcernConstructor | null { return this.#concern; } diff --git a/packages/support/src/concerns/exceptions/InjectionError.ts b/packages/support/src/concerns/exceptions/InjectionError.ts index f4783fcc..d0038129 100644 --- a/packages/support/src/concerns/exceptions/InjectionError.ts +++ b/packages/support/src/concerns/exceptions/InjectionError.ts @@ -23,13 +23,13 @@ export default class InjectionError extends ConcernError implements InjectionExc * Create a new Injection Error instance * * @param {ConstructorOrAbstractConstructor | MustUseConcerns} target - * @param {ConcernConstructor} concern + * @param {ConcernConstructor | null} concern * @param {string} message * @param {ErrorOptions} [options] */ constructor( target: ConstructorOrAbstractConstructor | MustUseConcerns, - concern: ConcernConstructor, + concern: ConcernConstructor | null, message: string, options?: ErrorOptions ) { From cc4d02666a51b7b8de95b935e4c4102331f892ed Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 20:02:29 +0100 Subject: [PATCH 298/424] Add defineConcerns() implementation --- .../support/src/concerns/ConcernsInjector.ts | 127 ++++++++++++++---- 1 file changed, 100 insertions(+), 27 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 098aa5d5..71eda2ed 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -5,8 +5,19 @@ import type { MustUseConcerns, Configuration, } from "@aedart/contracts/support/concerns"; -import { ALWAYS_HIDDEN } from "@aedart/contracts/support/concerns"; -import type { Constructor } from "@aedart/contracts"; +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { + ALWAYS_HIDDEN, + CONCERN_CLASSES +} from "@aedart/contracts/support/concerns"; +import { + getAllParentsOfClass, + getNameOrDesc +} from "@aedart/support/reflections"; +import { + AlreadyRegisteredError, + InjectionError +} from "./exceptions"; /** * TODO: Incomplete @@ -88,29 +99,62 @@ export default class ConcernsInjector implements Injector // // return this.target as MustUseConcerns; // } - // - // /** - // * Defines the concern classes that must be used by the target class. - // * - // * **Note**: _Method changes the target class, such that it implements and respects the - // * {@link MustUseConcerns} interface. The original target class' constructor remains the untouched!_ - // * - // * @template T = object - // * - // * @param {T} target The target class that must define the concern classes to be used - // * @param {Constructor[]} concerns List of concern classes - // * - // * @returns {MustUseConcerns} The modified target class - // * - // * @throws {InjectionException} If given concern classes conflict with target class' parent concern classes, - // * e.g. in case of duplicates. Or, if unable to modify target class. - // */ - // public defineConcerns(target: T, concerns: Constructor[]): MustUseConcerns - // { - // // TODO: implement this method... - // - // return target as MustUseConcerns; - // } + + /** + * Defines the concern classes that must be used by the target class. + * + * **Note**: _Method changes the target class, such that it implements and respects the + * {@link MustUseConcerns} interface._ + * + * @template C extends Concern + * @template T = object + * + * @param {T} target The target class that must define the concern classes to be used + * @param {Constructor[]} concerns List of concern classes + * + * @returns {MustUseConcerns} The modified target class + * + * @throws {AlreadyRegisteredError} If given concern classes conflict with target class' parent concern classes, + * e.g. in case of duplicates. + * @throws {InjectionError} If unable to register concern classes in target class + */ + public defineConcerns(target: T, concerns: ConcernConstructor[]): MustUseConcerns + { + // Obtain evt. previous defined concern classes in target. + const alreadyRegistered: ConcernConstructor[] = (Reflect.has(target as object, CONCERN_CLASSES)) + ? target[CONCERN_CLASSES as keyof typeof target] as ConcernConstructor[] + : []; + + // Make a new list with already registered concern classes + const register: ConcernConstructor[] = [ ...alreadyRegistered ]; + + // Loop through provided concerns and add them to the new list + for (const concern of concerns) { + // Fail if concern is already registered + if (register.includes(concern)) { + const source = this.findSourceOf(concern, target as object); + throw new AlreadyRegisteredError(target as ConstructorOrAbstractConstructor, concern, source as ConstructorOrAbstractConstructor); + } + + register.push(concern); + } + + // Define static property that contains concern classes in target. + const wasDefined: boolean = Reflect.defineProperty(target as object, CONCERN_CLASSES, { + get: function() { + return register; + } + }); + + if (!wasDefined) { + const reason: string = `Unable to define concern classes in target ${getNameOrDesc(target as ConstructorOrAbstractConstructor)}`; + throw new InjectionError(target as ConstructorOrAbstractConstructor, null, reason); + } + + // Finally, return the modified target... + return target as MustUseConcerns; + } + // // /** // * Defines a concerns {@link Container} in target class' prototype. @@ -186,8 +230,37 @@ export default class ConcernsInjector implements Injector // // return false; // } - - + + + /***************************************************************** + * Internals + ****************************************************************/ + + /** + * Find the source class where given concern is registered + * + * @param {ConcernConstructor} concern + * @param {object} target + * @param {boolean} [includeTarget=false] + * + * @returns {object | null} The source class, e.g. parent of target class, or `null` if concern is not registered + * in target or target's parents. + * + * @protected + */ + protected findSourceOf(concern: ConcernConstructor, target: object, includeTarget: boolean = false): object | null + { + const parents = getAllParentsOfClass(target as ConstructorOrAbstractConstructor, includeTarget).reverse(); + + for (const parent of parents) { + if (Reflect.has(target, CONCERN_CLASSES) && (target[CONCERN_CLASSES as keyof typeof target] as ConcernConstructor[]).includes(concern)) { + return parent; + } + } + + return null; + } + // ------------------------------------------------------------------------- // // TODO: Was previously part of AbstractConcern, but the logic should belong here // TODO: instead... From 98cd1267fd8ff402598069da4d2e59d95d84adae Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 20:05:39 +0100 Subject: [PATCH 299/424] Simplify exception thrown Name of exceptions are easy enough to understand - no need for more descriptions. --- packages/contracts/src/support/concerns/Injector.ts | 5 ++--- packages/support/src/concerns/ConcernsInjector.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index 86ba0830..a0ab7a8d 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -58,9 +58,8 @@ export default interface Injector * * @returns {MustUseConcerns} The modified target class * - * @throws {AlreadyRegisteredException} If given concern classes conflict with target class' parent concern classes, - * e.g. in case of duplicates. - * @throws {InjectionException} If unable to register concern classes in target class + * @throws {AlreadyRegisteredException} + * @throws {InjectionException} */ defineConcerns(target: T, concerns: ConcernConstructor[]): MustUseConcerns; diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 71eda2ed..0f39d489 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -114,9 +114,8 @@ export default class ConcernsInjector implements Injector * * @returns {MustUseConcerns} The modified target class * - * @throws {AlreadyRegisteredError} If given concern classes conflict with target class' parent concern classes, - * e.g. in case of duplicates. - * @throws {InjectionError} If unable to register concern classes in target class + * @throws {AlreadyRegisteredError} + * @throws {InjectionError} */ public defineConcerns(target: T, concerns: ConcernConstructor[]): MustUseConcerns { From 8b1932be21a3b69fef8a6a9ed8c0e2a8c94f81e3 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 20:20:36 +0100 Subject: [PATCH 300/424] Simplify defineConcerns(), remove C generic type --- packages/contracts/src/support/concerns/Injector.ts | 5 ++--- packages/support/src/concerns/ConcernsInjector.ts | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index a0ab7a8d..cc86ad40 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -49,8 +49,7 @@ export default interface Injector * * **Note**: _Method changes the target class, such that it implements and respects the * {@link MustUseConcerns} interface._ - * - * @template C extends Concern + * * @template T = object * * @param {T} target The target class that must define the concern classes to be used @@ -61,7 +60,7 @@ export default interface Injector * @throws {AlreadyRegisteredException} * @throws {InjectionException} */ - defineConcerns(target: T, concerns: ConcernConstructor[]): MustUseConcerns; + defineConcerns(target: T, concerns: ConcernConstructor[]): MustUseConcerns; /** * Defines a concerns {@link Container} in target class' prototype. diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 0f39d489..5fb460b7 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -106,7 +106,6 @@ export default class ConcernsInjector implements Injector * **Note**: _Method changes the target class, such that it implements and respects the * {@link MustUseConcerns} interface._ * - * @template C extends Concern * @template T = object * * @param {T} target The target class that must define the concern classes to be used @@ -117,7 +116,7 @@ export default class ConcernsInjector implements Injector * @throws {AlreadyRegisteredError} * @throws {InjectionError} */ - public defineConcerns(target: T, concerns: ConcernConstructor[]): MustUseConcerns + public defineConcerns(target: T, concerns: ConcernConstructor[]): MustUseConcerns { // Obtain evt. previous defined concern classes in target. const alreadyRegistered: ConcernConstructor[] = (Reflect.has(target as object, CONCERN_CLASSES)) From 5d520e4f6f67171a2eb2626a3d643fa5765cf437 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 20:25:21 +0100 Subject: [PATCH 301/424] Refactor, extract define static prop logic into own method --- .../support/src/concerns/ConcernsInjector.ts | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 5fb460b7..e9f19573 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -124,33 +124,21 @@ export default class ConcernsInjector implements Injector : []; // Make a new list with already registered concern classes - const register: ConcernConstructor[] = [ ...alreadyRegistered ]; + const registry: ConcernConstructor[] = [ ...alreadyRegistered ]; // Loop through provided concerns and add them to the new list for (const concern of concerns) { // Fail if concern is already registered - if (register.includes(concern)) { + if (registry.includes(concern)) { const source = this.findSourceOf(concern, target as object); throw new AlreadyRegisteredError(target as ConstructorOrAbstractConstructor, concern, source as ConstructorOrAbstractConstructor); } - register.push(concern); + registry.push(concern); } - // Define static property that contains concern classes in target. - const wasDefined: boolean = Reflect.defineProperty(target as object, CONCERN_CLASSES, { - get: function() { - return register; - } - }); - - if (!wasDefined) { - const reason: string = `Unable to define concern classes in target ${getNameOrDesc(target as ConstructorOrAbstractConstructor)}`; - throw new InjectionError(target as ConstructorOrAbstractConstructor, null, reason); - } - - // Finally, return the modified target... - return target as MustUseConcerns; + // Finally, define concern classes property + return this.defineConcernClassesProperty(target, registry); } // @@ -234,6 +222,38 @@ export default class ConcernsInjector implements Injector * Internals ****************************************************************/ + /** + * Defines the {@link CONCERN_CLASSES} static property in given target + * + * @template T = object + * + * @param {T} target + * @param {ConcernConstructor[]} registry + * + * @returns {MustUseConcerns} + * + * @throws {InjectionError} + * + * @protected + */ + protected defineConcernClassesProperty(target: T, registry: ConcernConstructor[]): MustUseConcerns + { + // Define static property that contains concern classes in target. + const wasDefined: boolean = Reflect.defineProperty(target as object, CONCERN_CLASSES, { + get: function() { + return registry; + } + }); + + if (!wasDefined) { + const reason: string = `Unable to define concern classes in target ${getNameOrDesc(target as ConstructorOrAbstractConstructor)}`; + throw new InjectionError(target as ConstructorOrAbstractConstructor, null, reason); + } + + // Finally, return the modified target... + return target as MustUseConcerns; + } + /** * Find the source class where given concern is registered * From 27d3514bfe4ead0419b26f39b168d32144adc0f1 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 20:26:42 +0100 Subject: [PATCH 302/424] Fix style --- packages/support/src/concerns/ConcernsInjector.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index e9f19573..3f7cbfc3 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -122,12 +122,11 @@ export default class ConcernsInjector implements Injector const alreadyRegistered: ConcernConstructor[] = (Reflect.has(target as object, CONCERN_CLASSES)) ? target[CONCERN_CLASSES as keyof typeof target] as ConcernConstructor[] : []; - - // Make a new list with already registered concern classes + + // Make a registry of concern classes to be registered in given target const registry: ConcernConstructor[] = [ ...alreadyRegistered ]; - - // Loop through provided concerns and add them to the new list for (const concern of concerns) { + // Fail if concern is already registered if (registry.includes(concern)) { const source = this.findSourceOf(concern, target as object); From 856921014133d1c145bb034bdefb8242ec61a4a1 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 20:40:10 +0100 Subject: [PATCH 303/424] Fix import --- .../support/src/concerns/exceptions/AlreadyRegisteredError.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts b/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts index 57bf9fbe..d04e59bb 100644 --- a/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts +++ b/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts @@ -6,7 +6,7 @@ import type { import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import { configureCustomError } from "@aedart/support/exceptions"; import { getNameOrDesc } from "@aedart/support/reflections"; -import { InjectionError } from "@aedart/support/concerns"; +import InjectionError from "./InjectionError"; /** * Already Registered Error From c9b126e770613276c9cb1b0a4fd1068c95f1b6c8 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 20:40:36 +0100 Subject: [PATCH 304/424] Add helper for making new Concerns Injector in tests --- .../concerns/helpers/makeConcernsInjector.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/browser/packages/support/concerns/helpers/makeConcernsInjector.js diff --git a/tests/browser/packages/support/concerns/helpers/makeConcernsInjector.js b/tests/browser/packages/support/concerns/helpers/makeConcernsInjector.js new file mode 100644 index 00000000..79379084 --- /dev/null +++ b/tests/browser/packages/support/concerns/helpers/makeConcernsInjector.js @@ -0,0 +1,13 @@ +import { ConcernsInjector } from "@aedart/support/concerns"; + +/** + * Returns a new Concerns Injector instance + * + * @param {object} target + * + * @returns {ConcernsInjector} + */ +export default function makeConcernsInjector(target) +{ + return new ConcernsInjector(target); +} \ No newline at end of file From 857c8cfe014f1f6198f532a0b8d2fdd5ffeff760 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 20:40:58 +0100 Subject: [PATCH 305/424] Add high level tests for concerns injector (incomplete) --- .../support/concerns/ConcernsInjector.test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/browser/packages/support/concerns/ConcernsInjector.test.js diff --git a/tests/browser/packages/support/concerns/ConcernsInjector.test.js b/tests/browser/packages/support/concerns/ConcernsInjector.test.js new file mode 100644 index 00000000..be841df5 --- /dev/null +++ b/tests/browser/packages/support/concerns/ConcernsInjector.test.js @@ -0,0 +1,18 @@ +import { ConcernsInjector } from "@aedart/support/concerns"; +import makeConcernsInjector from "./helpers/makeConcernsInjector"; + +describe('@aedart/support/concerns', () => { + describe('ConcernsInjector', () => { + + it('can make new instance', () => { + const injector = makeConcernsInjector(class {}); + + // Debug + // console.log('result', injector); + + expect(injector) + .toBeInstanceOf(ConcernsInjector); + }); + + }); +}); \ No newline at end of file From 84f55f787c67e759fbce69abafd9a63a4408eaa1 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 21:10:03 +0100 Subject: [PATCH 306/424] Fix incorrect source in Already Registered Error --- packages/support/src/concerns/ConcernsInjector.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 3f7cbfc3..8a44b9fe 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -129,7 +129,7 @@ export default class ConcernsInjector implements Injector // Fail if concern is already registered if (registry.includes(concern)) { - const source = this.findSourceOf(concern, target as object); + const source = this.findSourceOf(concern, target as object, true); throw new AlreadyRegisteredError(target as ConstructorOrAbstractConstructor, concern, source as ConstructorOrAbstractConstructor); } @@ -270,7 +270,7 @@ export default class ConcernsInjector implements Injector const parents = getAllParentsOfClass(target as ConstructorOrAbstractConstructor, includeTarget).reverse(); for (const parent of parents) { - if (Reflect.has(target, CONCERN_CLASSES) && (target[CONCERN_CLASSES as keyof typeof target] as ConcernConstructor[]).includes(concern)) { + if (Reflect.has(parent, CONCERN_CLASSES) && (parent[CONCERN_CLASSES as keyof typeof parent] as ConcernConstructor[]).includes(concern)) { return parent; } } From da1a04d43af997536e5798cd0ad1444962c5879f Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 21:10:16 +0100 Subject: [PATCH 307/424] Improve error message --- .../support/src/concerns/exceptions/AlreadyRegisteredError.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts b/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts index d04e59bb..52399e22 100644 --- a/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts +++ b/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts @@ -33,7 +33,9 @@ export default class AlreadyRegisteredError extends InjectionError implements Al message?: string, options?: ErrorOptions ) { - const resolved = message || `Concern ${getNameOrDesc(concern)} is already registered in class ${getNameOrDesc(target)} (via class ${getNameOrDesc(target)})`; + const resolved = message || (target === source) + ? `Concern ${getNameOrDesc(concern)} is already registered in class ${getNameOrDesc(target)}` + : `Concern ${getNameOrDesc(concern)} is already registered in class ${getNameOrDesc(target)} (via parent class ${getNameOrDesc(source)})` super(target, concern, resolved, options); From 4f059467e369addb1eeca278944463307b57d782 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 23 Feb 2024 21:20:56 +0100 Subject: [PATCH 308/424] Add unit test for Injector's defineConcerns() method --- .../concerns/injector/defineConcerns.test.js | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 tests/browser/packages/support/concerns/injector/defineConcerns.test.js diff --git a/tests/browser/packages/support/concerns/injector/defineConcerns.test.js b/tests/browser/packages/support/concerns/injector/defineConcerns.test.js new file mode 100644 index 00000000..dcc7c960 --- /dev/null +++ b/tests/browser/packages/support/concerns/injector/defineConcerns.test.js @@ -0,0 +1,158 @@ +import { AbstractConcern, AlreadyRegisteredError, InjectionError } from "@aedart/support/concerns"; +import { CONCERN_CLASSES } from "@aedart/contracts/support/concerns"; +import makeConcernsInjector from "../helpers/makeConcernsInjector"; + +describe('@aedart/support/concerns', () => { + describe('ConcernsInjector', () => { + describe('defineConcerns()', () => { + + it('can define concerns classes in target', () => { + class ConcernA extends AbstractConcern {} + class ConcernB extends AbstractConcern {} + class ConcernC extends AbstractConcern {} + + class A {} + + const injector = makeConcernsInjector(A); + + injector.defineConcerns(A, [ + ConcernA, + ConcernB, + ConcernC, + ]); + + // ------------------------------------------------------------------- // + + expect(Reflect.has(A, CONCERN_CLASSES)) + .withContext('Target does not have static concern classes property defined') + .toBeTrue(); + + const registered = A[CONCERN_CLASSES]; + expect(Array.isArray(registered)) + .withContext('static concern classes property is not an array') + .toBeTrue(); + + expect(registered) + .withContext('incorrect concern classes registered') + .toEqual([ ConcernA, ConcernB, ConcernC ]); + }); + + it('inherits already registered concerns from parent', () => { + class ConcernA extends AbstractConcern {} + class ConcernB extends AbstractConcern {} + class ConcernC extends AbstractConcern {} + + class A {} + + class B extends A {} + + const injector = makeConcernsInjector(A); + + // Define for A + injector.defineConcerns(A, [ + ConcernA, + ConcernB + ]); + + // Define for B + injector.defineConcerns(B, [ + ConcernC + ]); + + // ------------------------------------------------------------------- // + + expect(Reflect.has(B, CONCERN_CLASSES)) + .withContext('Target B does not have static concern classes property defined') + .toBeTrue(); + + const registryB = B[CONCERN_CLASSES]; + + // Debug + // console.log('registry b', registryB); + + expect(registryB) + .withContext('Target B did not inherit concern classes from parent class') + .toEqual([ ConcernA, ConcernB, ConcernC ]); + + // ------------------------------------------------------------------- // + + // Ensure that A's concern classes have not been altered + const registryA = A[CONCERN_CLASSES]; + + // Debug + // console.log('registry a', registryA); + + expect(registryA) + .withContext('Target A concern classes has been modified - BUT SHOULD NOT BE!') + .toEqual([ ConcernA, ConcernB ]); + }); + + it('fails when attempting to register same concern twice in target', () => { + class ConcernA extends AbstractConcern {} + + class A {} + + const injector = makeConcernsInjector(A); + + const callback = () => { + injector.defineConcerns(A, [ + ConcernA, + ConcernA, // Should cause error + ]); + } + + // ------------------------------------------------------------------- // + + expect(callback) + .toThrowError(AlreadyRegisteredError); + }); + + it('fails to register concern class when already registered in parent', () => { + class ConcernA extends AbstractConcern {} + + class A {} + + class B extends A {} + + const injector = makeConcernsInjector(A); + + // Define for A + injector.defineConcerns(A, [ + ConcernA + ]); + + // Define for B + const callback = () => { + injector.defineConcerns(B, [ + ConcernA // Should cause error + ]); + } + + // ------------------------------------------------------------------- // + + expect(callback) + .toThrowError(AlreadyRegisteredError); + }); + + it('fails if unable to define static concern classes property', () => { + class ConcernA extends AbstractConcern {} + + class A {} + Object.freeze(A); // This can prevent static property from being added + + const injector = makeConcernsInjector(A); + + const callback = () => { + injector.defineConcerns(A, [ + ConcernA + ]); + } + + // ------------------------------------------------------------------- // + + expect(callback) + .toThrowError(InjectionError); + }); + }); + }); +}); \ No newline at end of file From 435939b23e209b5fb35385df2fb59493ebf68200 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 10:41:36 +0100 Subject: [PATCH 309/424] Add defineContainer() implementation --- .../support/src/concerns/ConcernsInjector.ts | 76 +++++-- .../concerns/injector/defineContainer.test.js | 199 ++++++++++++++++++ 2 files changed, 252 insertions(+), 23 deletions(-) create mode 100644 tests/browser/packages/support/concerns/injector/defineContainer.test.js diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 8a44b9fe..6ea76bb1 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -4,11 +4,14 @@ import type { Injector, MustUseConcerns, Configuration, + Owner, + Container, } from "@aedart/contracts/support/concerns"; import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import { ALWAYS_HIDDEN, - CONCERN_CLASSES + CONCERN_CLASSES, + CONCERNS } from "@aedart/contracts/support/concerns"; import { getAllParentsOfClass, @@ -18,6 +21,16 @@ import { AlreadyRegisteredError, InjectionError } from "./exceptions"; +import ConcernsContainer from './ConcernsContainer'; + +/** + * A map of the concern owner instances and their concerns container + * + * @internal + * + * @type {WeakMap} + */ +const CONTAINERS_REGISTRY: WeakMap = new WeakMap(); /** * TODO: Incomplete @@ -139,29 +152,46 @@ export default class ConcernsInjector implements Injector // Finally, define concern classes property return this.defineConcernClassesProperty(target, registry); } + + /** + * Defines a concerns {@link Container} in target class' prototype. + * + * **Note**: _Method changes the target class, such that it implements and respects the + * [Owner]{@link import('@aedart/contracts/support/concerns').Owner} interface!_ + * + * @template T = object + * + * @param {MustUseConcerns} target The target in which a concerns container must be defined + * + * @returns {MustUseConcerns} The modified target class + * + * @throws {InjectionError} If unable to define concerns container in target class + */ + public defineContainer(target: MustUseConcerns): MustUseConcerns + { + const concerns: ConcernConstructor[] = target[CONCERN_CLASSES]; + + // Define concerns property in target's prototype + const wasDefined: boolean = Reflect.defineProperty(target.prototype, CONCERNS, { + get: function() { + const instance: T & Owner = this; // This = target instance + + if (!CONTAINERS_REGISTRY.has(instance)) { + CONTAINERS_REGISTRY.set(instance, new ConcernsContainer(instance, concerns)); + } + + return CONTAINERS_REGISTRY.get(instance); + } + }); + + if (!wasDefined) { + const reason: string = `Unable to define concerns container in target ${getNameOrDesc(target as ConstructorOrAbstractConstructor)}`; + throw new InjectionError(target as ConstructorOrAbstractConstructor, null, reason); + } + + return target; + } - // - // /** - // * Defines a concerns {@link Container} in target class' prototype. - // * - // * **Note**: _Method changes the target class, such that it implements and respects the - // * [Owner]{@link import('@aedart/contracts/support/concerns').Owner} interface!_ - // * - // * @template T = object - // * - // * @param {MustUseConcerns} target The target in which a concerns container must be defined - // * - // * @returns {MustUseConcerns} The modified target class - // * - // * @throws {InjectionException} If unable to define concerns container in target class - // */ - // public defineContainer(target: MustUseConcerns): MustUseConcerns - // { - // // TODO: implement this method... - // - // return target; - // } - // // /** // * Defines "aliases" (proxy properties and methods) in target class' prototype, such that they // * point to the properties and methods available in the concern classes. diff --git a/tests/browser/packages/support/concerns/injector/defineContainer.test.js b/tests/browser/packages/support/concerns/injector/defineContainer.test.js new file mode 100644 index 00000000..03d2d379 --- /dev/null +++ b/tests/browser/packages/support/concerns/injector/defineContainer.test.js @@ -0,0 +1,199 @@ +import { CONCERNS } from "@aedart/contracts/support/concerns"; +import { AbstractConcern, ConcernsContainer, InjectionError } from "@aedart/support/concerns"; +import makeConcernsInjector from "../helpers/makeConcernsInjector"; + +describe('@aedart/support/concerns', () => { + describe('ConcernsInjector', () => { + describe('defineContainer()', () => { + + it('can define concerns container in target prototype', () => { + class ConcernA extends AbstractConcern {} + + class A {} + + const injector = makeConcernsInjector(A); + + const target = injector.defineContainer( + injector.defineConcerns(A, [ + ConcernA, + ]) + ); + + // ------------------------------------------------------------------------------------------ // + + expect(Reflect.has(target.prototype, CONCERNS)) + .withContext('Concerns container property not defined in target prototype') + .toBeTrue(); + }); + + it('container instance is created when obtained from target instance', () => { + class ConcernA extends AbstractConcern {} + class ConcernB extends AbstractConcern {} + class ConcernC extends AbstractConcern {} + + class A {} + + const injector = makeConcernsInjector(A); + + const target = injector.defineContainer( + injector.defineConcerns(A, [ + ConcernA, + ConcernB, + ConcernC, + ]) + ); + + // ------------------------------------------------------------------------------------------ // + + const instance = new target(); + const container = instance[CONCERNS]; + + // Debug + // console.log('container', container); + + expect(container) + .withContext('Invalid concerns container instance') + .toBeInstanceOf(ConcernsContainer); + + expect(container.isNotEmpty()) + .withContext('Container has no concerns registered') + .toBeTrue(); + + expect(container.has(ConcernA)) + .withContext('Concern A not registered in container') + .toBeTrue(); + expect(container.has(ConcernB)) + .withContext('Concern B not registered in container') + .toBeTrue(); + expect(container.has(ConcernC)) + .withContext('Concern C not registered in container') + .toBeTrue(); + }); + + it('does not inherit container from parent instance', () => { + class ConcernA extends AbstractConcern {} + class ConcernB extends AbstractConcern {} + class ConcernC extends AbstractConcern {} + + class A {} + class B extends A {} + + const injector = makeConcernsInjector(A); + + const targetA = injector.defineContainer( + injector.defineConcerns(A, [ + ConcernA, + ConcernB, + ]) + ); + + const targetB = injector.defineContainer( + injector.defineConcerns(B, [ + ConcernC + ]) + ); + + // ------------------------------------------------------------------------------------------ // + + const instanceA = new targetA(); + const instanceB = new targetB(); + + const containerA = instanceA[CONCERNS]; + const containerB = instanceB[CONCERNS]; + + expect(containerA === containerB) + .withContext('Same container instance returned for different target instances!') + .toBeFalse(); + + expect(containerA.size) + .withContext('Incorrect amount of concerns in container a') + .toBe(2); + + expect(containerB.size) + .withContext('Incorrect amount of concerns in container b') + .toBe(3); + }); + + it('can interact with concerns in container', () => { + class ConcernA extends AbstractConcern { + foo() { + return 'foo'; + } + } + class ConcernB extends AbstractConcern { + get bar() { + return 'bar' + } + } + class ConcernC extends AbstractConcern { + #msg = null; + + set message(value) { + this.#msg = value; + } + + get message() { + return this.#msg; + } + } + + class A {} + + const injector = makeConcernsInjector(A); + + const target = injector.defineContainer( + injector.defineConcerns(A, [ + ConcernA, + ConcernB, + ConcernC, + ]) + ); + + // ------------------------------------------------------------------------------------------ // + + const instance = new target(); + const container = instance[CONCERNS]; + + // Debug + // console.log('container', container); + + expect(container.call(ConcernA, 'foo')) + .withContext('Unable to call method in concern a') + .toBe('foo'); + + expect(container.getProperty(ConcernB, 'bar')) + .withContext('Unable to get property from concern b') + .toBe('bar'); + + const msg = 'Sweet...'; + container.setProperty(ConcernC, 'message', msg); + expect(container.getProperty(ConcernC, 'message')) + .withContext('Unable to set and get property in concern c') + .toBe(msg); + }); + + it('fails if unable to define concerns container property in target prototype', () => { + class ConcernA extends AbstractConcern {} + + class A {} + + const injector = makeConcernsInjector(A); + + const target = injector.defineConcerns(A, [ + ConcernA, + ]) + + Object.freeze(target.prototype); // This should cause an error whe attempting to define container... + + const callback = () => { + injector.defineContainer(target); + } + + // ------------------------------------------------------------------------------------------ // + + expect(callback) + .toThrowError(InjectionError); + }); + }); + }); +}); \ No newline at end of file From 6d9c9208c52affdf042815cb223a560a05218e68 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 11:35:36 +0100 Subject: [PATCH 310/424] Refactor, extract define property in target into own method Also extracted concerns registry into own method. This will most likely change again, as we need to normalize it with concerns configuration. --- .../support/src/concerns/ConcernsInjector.ts | 111 +++++++++++------- 1 file changed, 68 insertions(+), 43 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 6ea76bb1..f72a40dc 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -131,26 +131,13 @@ export default class ConcernsInjector implements Injector */ public defineConcerns(target: T, concerns: ConcernConstructor[]): MustUseConcerns { - // Obtain evt. previous defined concern classes in target. - const alreadyRegistered: ConcernConstructor[] = (Reflect.has(target as object, CONCERN_CLASSES)) - ? target[CONCERN_CLASSES as keyof typeof target] as ConcernConstructor[] - : []; - - // Make a registry of concern classes to be registered in given target - const registry: ConcernConstructor[] = [ ...alreadyRegistered ]; - for (const concern of concerns) { - - // Fail if concern is already registered - if (registry.includes(concern)) { - const source = this.findSourceOf(concern, target as object, true); - throw new AlreadyRegisteredError(target as ConstructorOrAbstractConstructor, concern, source as ConstructorOrAbstractConstructor); - } - - registry.push(concern); - } + const registry = this.resolveConcernsRegistry(target as object, concerns); - // Finally, define concern classes property - return this.defineConcernClassesProperty(target, registry); + return this.definePropertyInTarget(target, CONCERN_CLASSES, { + get: function() { + return registry; + } + }) as MustUseConcerns; } /** @@ -170,11 +157,11 @@ export default class ConcernsInjector implements Injector public defineContainer(target: MustUseConcerns): MustUseConcerns { const concerns: ConcernConstructor[] = target[CONCERN_CLASSES]; - - // Define concerns property in target's prototype - const wasDefined: boolean = Reflect.defineProperty(target.prototype, CONCERNS, { + + this.definePropertyInTarget(target.prototype, CONCERNS, { get: function() { - const instance: T & Owner = this; // This = target instance + // @ts-expect-error This = target instance. TypeScript just doesn't understand context here... + const instance: T & Owner = this; if (!CONTAINERS_REGISTRY.has(instance)) { CONTAINERS_REGISTRY.set(instance, new ConcernsContainer(instance, concerns)); @@ -183,11 +170,6 @@ export default class ConcernsInjector implements Injector return CONTAINERS_REGISTRY.get(instance); } }); - - if (!wasDefined) { - const reason: string = `Unable to define concerns container in target ${getNameOrDesc(target as ConstructorOrAbstractConstructor)}`; - throw new InjectionError(target as ConstructorOrAbstractConstructor, null, reason); - } return target; } @@ -252,35 +234,78 @@ export default class ConcernsInjector implements Injector ****************************************************************/ /** - * Defines the {@link CONCERN_CLASSES} static property in given target + * Resolves the concern classes to be registered (registry), for the given target * - * @template T = object + * **Note**: _Method ensures that if target already has concern classes defined, then those + * are merged into the resulting list._ * - * @param {T} target - * @param {ConcernConstructor[]} registry + * @param {object} target + * @param {ConcernConstructor[]} concerns + * + * @returns {ConcernConstructor[]} Registry with concern classes that are ready to be registered in given target * - * @returns {MustUseConcerns} + * @throws {AlreadyRegisteredError} If a concern has already been registered in the target * + * @protected + */ + protected resolveConcernsRegistry(target: object, concerns: ConcernConstructor[]): ConcernConstructor[] + { + // Obtain evt. previous defined concern classes in target. + const alreadyRegistered: ConcernConstructor[] = (Reflect.has(target as object, CONCERN_CLASSES)) + ? target[CONCERN_CLASSES as keyof typeof target] as ConcernConstructor[] + : []; + + // Make a registry of concern classes to be registered in given target + const registry: ConcernConstructor[] = [ ...alreadyRegistered ]; + for (const concern of concerns) { + + // Fail if concern is already registered + if (registry.includes(concern)) { + const source = this.findSourceOf(concern, target as object, true); + throw new AlreadyRegisteredError(target as ConstructorOrAbstractConstructor, concern, source as ConstructorOrAbstractConstructor); + } + + registry.push(concern); + } + + return registry; + } + + /** + * Defines a property in given target + * + * @template T = object + * + * @param {T} target + * @param {PropertyKey} property + * @param {PropertyDescriptor} descriptor + * @param {string} [failMessage] + * + * @returns {T} + * * @throws {InjectionError} * * @protected */ - protected defineConcernClassesProperty(target: T, registry: ConcernConstructor[]): MustUseConcerns + protected definePropertyInTarget( + target: T, + property: PropertyKey, + descriptor: PropertyDescriptor, + failMessage?: string + ): T { - // Define static property that contains concern classes in target. - const wasDefined: boolean = Reflect.defineProperty(target as object, CONCERN_CLASSES, { - get: function() { - return registry; - } - }); + const wasDefined: boolean = Reflect.defineProperty((target as object), property, descriptor); if (!wasDefined) { - const reason: string = `Unable to define concern classes in target ${getNameOrDesc(target as ConstructorOrAbstractConstructor)}`; + const key = typeof property == 'symbol' + ? property.description + : property.toString(); + + const reason: string = failMessage || `Unable to define "${key}" property in target ${getNameOrDesc(target as ConstructorOrAbstractConstructor)}`; throw new InjectionError(target as ConstructorOrAbstractConstructor, null, reason); } - // Finally, return the modified target... - return target as MustUseConcerns; + return target; } /** From 6118babba2602bf7f65008b7372de27475a5abb4 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 12:05:45 +0100 Subject: [PATCH 311/424] Redesign Concern Configuration interface Removed the hidden property because it added unnecessary complexity. Also, switched to use Concern Constructor interface. --- .../src/support/concerns/Configuration.ts | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/packages/contracts/src/support/concerns/Configuration.ts b/packages/contracts/src/support/concerns/Configuration.ts index 04eb6dc4..5895c105 100644 --- a/packages/contracts/src/support/concerns/Configuration.ts +++ b/packages/contracts/src/support/concerns/Configuration.ts @@ -1,5 +1,5 @@ -import type { Constructor } from "@aedart/contracts"; import Concern from "./Concern"; +import ConcernConstructor from "./ConcernConstructor"; import type { Aliases } from "./types"; @@ -11,50 +11,47 @@ import type { * along with other aspects of the injection, such as what aliases to use * and what properties or methods should not be aliased. * + * @template T extends Concern = Concern + * * @see {Concern} * @see {Aliases} */ -export default interface Configuration +export default interface Configuration { /** * The Concern Class that must be injected into a target class * - * @type {Constructor} + * @template T extends Concern = Concern + * + * @type {ConcernConstructor} */ - concern: Constructor; + concern: ConcernConstructor; /** * Aliases for Concern's properties or methods. * - * **Note**: _An "injector" must always default to the same property and - * method names as those defined in the Concern Class, if a given property or - * method is not specified here._ + * **Note**: _Defaults to same property and method names as "aliases", as those defined by + * a concern class' [PROVIDES]{@link import('@aedart/contracts/support/concerns').PROVIDES}, + * if this property is `undefined`._ * - * @type {Aliases|undefined} - */ - aliases?: Aliases; - - /** - * Properties and methods that MUST NOT be aliased into a target class. - * - * **Note**: _Defaults to list provided by the Concern Class' [HIDDEN]{@link import('@aedart/contracts/support/concerns').HIDDEN}, - * if not specified here._ + * **Note**: _If an empty array is given, then it has the same effect as setting {@link allowAliases} + * to `false`._ * - * **Note**: _Properties and methods that are defined in [ALWAYS_HIDDEN]{@link import('@aedart/contracts/support/concerns').ALWAYS_HIDDEN} - * will always be hidden, regardless of those defined here._ + * @template T extends Concern = Concern * - * @type {PropertyKey[]|undefined} + * @type {Aliases|undefined} */ - hidden?: PropertyKey[]; + aliases?: Aliases; /** * Flag that indicates whether an "injector" is allowed to create * "aliases" (proxy) properties and methods into a target class's prototype. * - * If set to `false`, then {@link aliases} and {@link hidden} settings - * are ignored. + * **Note**: _Defaults to `true` if this property is `undefined`._ + * + * **Note**: _If set to `false`, then {@link aliases} are ignored._ * * @type {boolean} */ - allowAliases: boolean; + allowAliases?: boolean; } \ No newline at end of file From fbf237640e771d22d381721cb5898c3d564f9d5c Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 12:09:00 +0100 Subject: [PATCH 312/424] Change inject method, remove C generic This should no longer be needed. Both the Concern Constructor and Concern Configuration interfaces should handle this on their own. --- packages/contracts/src/support/concerns/Injector.ts | 5 ++--- packages/support/src/concerns/ConcernsInjector.ts | 6 ++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index cc86ad40..60b6bfb2 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -34,15 +34,14 @@ export default interface Injector * _**C**: Defines "aliases" (proxy properties and methods) in target class' prototype, via {@link defineAliases}._ * * @template T = object The target class that concern classes must be injected into - * @template C = {@link Concern} * - * @param {ConcernConstructor | Configuration} concerns List of concern classes / injection configurations + * @param {ConcernConstructor | Configuration} concerns List of concern classes / injection configurations * * @returns {MustUseConcerns} The modified target class * * @throws {InjectionException} */ - inject(...concerns: (ConcernConstructor|Configuration)[]): MustUseConcerns; + inject(...concerns: (ConcernConstructor|Configuration)[]): MustUseConcerns; /** * Defines the concern classes that must be used by the target class. diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index f72a40dc..bc995d71 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -76,7 +76,6 @@ export default class ConcernsInjector implements Injector } // TODO: INCOMPLETE... - // // /** // * Injects concern classes into the target class and return the modified target. @@ -90,15 +89,14 @@ export default class ConcernsInjector implements Injector // * _**C**: Defines "aliases" (proxy properties and methods) in target class' prototype, via {@link defineAliases}._ // * // * @template T = object The target class that concern classes must be injected into - // * @template C = {@link Concern} // * - // * @param {Constructor | Configuration} concerns List of concern classes / injection configurations + // * @param {ConcernConstructor | Configuration} concerns List of concern classes / injection configurations // * // * @returns {MustUseConcerns} The modified target class // * // * @throws {InjectionException} // */ - // public inject(...concerns: (Constructor|Configuration)[]): MustUseConcerns + // inject(...concerns: (ConcernConstructor|Configuration)[]): MustUseConcerns; // { // // TODO: implement this method... // From 76a2227c6e18cbb5463962c64b1b6939d3190d6f Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 12:19:49 +0100 Subject: [PATCH 313/424] Add normalise util (incomplete) --- .../support/src/concerns/ConcernsInjector.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index bc995d71..7a9d84a9 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -226,6 +226,40 @@ export default class ConcernsInjector implements Injector // return false; // } + /** + * Normalises given concerns into a list of concern configurations + * + * @param {(ConcernConstructor | Configuration)[]} concerns + * + * @returns {Configuration[]} + * + * @throws {InjectionError} + */ + public normalise(concerns: (ConcernConstructor|Configuration)[]): Configuration[] + { + const output: Configuration[] = []; + + for (const entry of concerns) { + output.push(this.resolveConfiguration(entry)); + } + + return output; + } + + // TODO: Incomplete + protected resolveConfiguration(entry: ConcernConstructor|Configuration): Configuration + { + // TODO: If a concern class is given, create a new configuration for it + + // TODO: If configuration is given, create a new one and merge given into it + + // TODO: Otherwise, fail! (entry is of unsupported type) + + // TODO: Remove unsafe property keys + + // TODO: Finally, return configuration + return {} as Configuration; + } /***************************************************************** * Internals From 286af6f8e018ec3f9be674b04216a48856af82f1 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 12:36:16 +0100 Subject: [PATCH 314/424] Change behavior description of aliases Should always just default to concern class' PROVIDES, which then can be overwritten. --- packages/contracts/src/support/concerns/Configuration.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/contracts/src/support/concerns/Configuration.ts b/packages/contracts/src/support/concerns/Configuration.ts index 5895c105..09a88be9 100644 --- a/packages/contracts/src/support/concerns/Configuration.ts +++ b/packages/contracts/src/support/concerns/Configuration.ts @@ -32,10 +32,7 @@ export default interface Configuration * * **Note**: _Defaults to same property and method names as "aliases", as those defined by * a concern class' [PROVIDES]{@link import('@aedart/contracts/support/concerns').PROVIDES}, - * if this property is `undefined`._ - * - * **Note**: _If an empty array is given, then it has the same effect as setting {@link allowAliases} - * to `false`._ + * if this property is empty or `undefined`._ * * @template T extends Concern = Concern * From 8f6dbadee6317481daf9439ceace63adaba9a854 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 12:37:38 +0100 Subject: [PATCH 315/424] Add interface for concern configuration factory --- .../contracts/src/support/concerns/Factory.ts | 32 +++++++++++++++++++ .../contracts/src/support/concerns/index.ts | 2 ++ 2 files changed, 34 insertions(+) create mode 100644 packages/contracts/src/support/concerns/Factory.ts diff --git a/packages/contracts/src/support/concerns/Factory.ts b/packages/contracts/src/support/concerns/Factory.ts new file mode 100644 index 00000000..a2b1ba4d --- /dev/null +++ b/packages/contracts/src/support/concerns/Factory.ts @@ -0,0 +1,32 @@ +import ConcernConstructor from "./ConcernConstructor"; +import Configuration from './Configuration'; + +/** + * Concern Configuration Factory + * + * Able to make new {@link Configuration} from a given concern "entry". + */ +export default interface Factory +{ + /** + * Returns a new normalised concern configuration for given concern "entry" + * + * **Note**: _"normalised" in this context means:_ + * + * _**A**: If a concern class is given, then a new concern configuration made._ + * + * _**B**: If configuration is given, then a new concern configuration and given + * configuration is merged into the new configuration._ + * + * _**C**: Configuration's `aliases` are automatically populated. When a concern + * configuration is provided, its evt. aliases are merged with the default ones, + * unless `allowAliases` is set to `false`, in which case all aliases are removed._ + * + * @param {ConcernConstructor | Configuration} entry + * + * @returns {Configuration} + * + * @throws {InjectionException} If entry is unsupported or invalid + */ + make(entry: ConcernConstructor | Configuration): Configuration; +} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 82b310cd..0db5cd7d 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -120,6 +120,7 @@ import Concern from "./Concern"; import ConcernConstructor from "./ConcernConstructor"; import Configuration from "./Configuration"; import Container from "./Container"; +import Factory from "./Factory"; import MustUseConcerns from "./MustUseConcerns"; import Injector from "./Injector"; import Owner from "./Owner"; @@ -128,6 +129,7 @@ export { type ConcernConstructor, type Configuration, type Container, + type Factory, type Injector, type MustUseConcerns, type Owner From bb85a81836c8975063ed07740a7dafa4f0fca52f Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 13:10:29 +0100 Subject: [PATCH 316/424] Add Unsafe Alias Error / Exception --- .../exceptions/UnsafeAliasException.ts | 27 ++++++ .../src/support/concerns/exceptions/index.ts | 4 +- .../concerns/exceptions/UnsafeAliasError.ts | 87 +++++++++++++++++++ .../support/src/concerns/exceptions/index.ts | 4 +- 4 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 packages/contracts/src/support/concerns/exceptions/UnsafeAliasException.ts create mode 100644 packages/support/src/concerns/exceptions/UnsafeAliasError.ts diff --git a/packages/contracts/src/support/concerns/exceptions/UnsafeAliasException.ts b/packages/contracts/src/support/concerns/exceptions/UnsafeAliasException.ts new file mode 100644 index 00000000..f2120789 --- /dev/null +++ b/packages/contracts/src/support/concerns/exceptions/UnsafeAliasException.ts @@ -0,0 +1,27 @@ +import InjectionException from "./InjectionException"; + +/** + * Unsafe Alias Exception + * + * To be thrown when an alias points to an "unsafe" property or method inside a concern. + */ +export default interface UnsafeAliasException extends InjectionException +{ + /** + * The alias that points to an "unsafe" property or method + * + * @readonly + * + * @type {PropertyKey} + */ + readonly alias: PropertyKey; + + /** + * The "unsafe" property or method that an alias points to + * + * @readonly + * + * @type {PropertyKey} + */ + readonly key: PropertyKey; +} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/exceptions/index.ts b/packages/contracts/src/support/concerns/exceptions/index.ts index 13e3bc4a..598c08be 100644 --- a/packages/contracts/src/support/concerns/exceptions/index.ts +++ b/packages/contracts/src/support/concerns/exceptions/index.ts @@ -3,10 +3,12 @@ import BootException from "./BootException"; import ConcernException from "./ConcernException"; import InjectionException from "./InjectionException"; import NotRegisteredException from "./NotRegisteredException"; +import UnsafeAliasException from "./UnsafeAliasException"; export { type AlreadyRegisteredException, type BootException, type ConcernException, type InjectionException, - type NotRegisteredException + type NotRegisteredException, + type UnsafeAliasException } \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/UnsafeAliasError.ts b/packages/support/src/concerns/exceptions/UnsafeAliasError.ts new file mode 100644 index 00000000..80881b96 --- /dev/null +++ b/packages/support/src/concerns/exceptions/UnsafeAliasError.ts @@ -0,0 +1,87 @@ +import type { + ConcernConstructor, + MustUseConcerns, UnsafeAliasException +} from "@aedart/contracts/support/concerns"; +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { configureCustomError } from "@aedart/support/exceptions"; +import InjectionError from "./InjectionError"; +import {getNameOrDesc} from "@aedart/support/reflections"; + +/** + * Unsafe Alias Error + */ +export default class UnsafeAliasError extends InjectionError implements UnsafeAliasException +{ + /** + * The alias that points to an "unsafe" property or method + * + * @readonly + * + * @type {PropertyKey} + */ + readonly #alias: PropertyKey; + + /** + * The "unsafe" property or method that an alias points to + * + * @readonly + * + * @type {PropertyKey} + */ + readonly #key: PropertyKey; + + /** + * Create a new Unsafe Alias Error instance + * + * @param {ConstructorOrAbstractConstructor | MustUseConcerns} target + * @param {ConcernConstructor} concern + * @param {PropertyKey} alias + * @param {PropertyKey} key + * @param {string} [message] + * @param {ErrorOptions} [options] + */ + constructor( + target: ConstructorOrAbstractConstructor | MustUseConcerns, + concern: ConcernConstructor, + alias: PropertyKey, + key: PropertyKey, + message?: string, + options?: ErrorOptions + ) { + const reason: string = message || `Alias ${alias.toString()} points to "unsafe" property or method ${key.toString()} in concern ${getNameOrDesc(concern)}, in target ${getNameOrDesc(target)}`; + super(target, concern, reason, options); + + configureCustomError(this); + + this.#alias = alias; + this.#key = key; + + // Force set the key and alias in the cause + (this.cause as Record).alias = alias; + (this.cause as Record).key = key; + } + + /** + * The alias that points to an "unsafe" property or method + * + * @readonly + * + * @type {PropertyKey} + */ + get alias(): PropertyKey + { + return this.#alias; + } + + /** + * The "unsafe" property or method that an alias points to + * + * @readonly + * + * @type {PropertyKey} + */ + get key(): PropertyKey + { + return this.#key; + } +} \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/index.ts b/packages/support/src/concerns/exceptions/index.ts index 8de018b8..57be44a4 100644 --- a/packages/support/src/concerns/exceptions/index.ts +++ b/packages/support/src/concerns/exceptions/index.ts @@ -3,10 +3,12 @@ import BootError from "./BootError"; import ConcernError from "./ConcernError"; import InjectionError from "./InjectionError"; import NotRegisteredError from "./NotRegisteredError"; +import UnsafeAliasError from "./UnsafeAliasError"; export { AlreadyRegisteredError, BootError, ConcernError, InjectionError, - NotRegisteredError + NotRegisteredError, + UnsafeAliasError }; \ No newline at end of file From 54d1dfc6ad1f17db39a7300660a5719c51eb71eb Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 13:11:46 +0100 Subject: [PATCH 317/424] Change factory, allow to throw Unsafe Alias Exception --- packages/contracts/src/support/concerns/Factory.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/Factory.ts b/packages/contracts/src/support/concerns/Factory.ts index a2b1ba4d..e69a6c41 100644 --- a/packages/contracts/src/support/concerns/Factory.ts +++ b/packages/contracts/src/support/concerns/Factory.ts @@ -10,7 +10,7 @@ export default interface Factory { /** * Returns a new normalised concern configuration for given concern "entry" - * + * * **Note**: _"normalised" in this context means:_ * * _**A**: If a concern class is given, then a new concern configuration made._ @@ -22,11 +22,13 @@ export default interface Factory * configuration is provided, its evt. aliases are merged with the default ones, * unless `allowAliases` is set to `false`, in which case all aliases are removed._ * + * @param {object} target * @param {ConcernConstructor | Configuration} entry * * @returns {Configuration} * + * @throws {UnsafeAliasException} If an alias points to an "unsafe" property or method in concern * @throws {InjectionException} If entry is unsupported or invalid */ - make(entry: ConcernConstructor | Configuration): Configuration; + make(target: object, entry: ConcernConstructor | Configuration): Configuration; } \ No newline at end of file From 28363582eb93772a4d7bd77b1716c04fea94ca37 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 13:17:53 +0100 Subject: [PATCH 318/424] Change internal TODO The "resolve config" method should now be able to use a factory and thereby be very simple in nature. --- packages/support/src/concerns/ConcernsInjector.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 7a9d84a9..d49b4a07 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -249,15 +249,8 @@ export default class ConcernsInjector implements Injector // TODO: Incomplete protected resolveConfiguration(entry: ConcernConstructor|Configuration): Configuration { - // TODO: If a concern class is given, create a new configuration for it + // TODO: Use Concern Configuration Factory to resolve configuration - // TODO: If configuration is given, create a new one and merge given into it - - // TODO: Otherwise, fail! (entry is of unsupported type) - - // TODO: Remove unsafe property keys - - // TODO: Finally, return configuration return {} as Configuration; } @@ -329,11 +322,7 @@ export default class ConcernsInjector implements Injector const wasDefined: boolean = Reflect.defineProperty((target as object), property, descriptor); if (!wasDefined) { - const key = typeof property == 'symbol' - ? property.description - : property.toString(); - - const reason: string = failMessage || `Unable to define "${key}" property in target ${getNameOrDesc(target as ConstructorOrAbstractConstructor)}`; + const reason: string = failMessage || `Unable to define "${property.toString()}" property in target ${getNameOrDesc(target as ConstructorOrAbstractConstructor)}`; throw new InjectionError(target as ConstructorOrAbstractConstructor, null, reason); } From 1575b5e5c866f46f5edfd589431d714121504123 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 13:21:32 +0100 Subject: [PATCH 319/424] Fix style --- packages/support/src/concerns/ConcernsInjector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index d49b4a07..a003a883 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -159,7 +159,7 @@ export default class ConcernsInjector implements Injector this.definePropertyInTarget(target.prototype, CONCERNS, { get: function() { // @ts-expect-error This = target instance. TypeScript just doesn't understand context here... - const instance: T & Owner = this; + const instance: T & Owner = this; /* eslint-disable-line @typescript-eslint/no-this-alias */ if (!CONTAINERS_REGISTRY.has(instance)) { CONTAINERS_REGISTRY.set(instance, new ConcernsContainer(instance, concerns)); From 9e0185b32284563a584f9422c33a7ad3efced8c7 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 18:33:28 +0100 Subject: [PATCH 320/424] Move responsibility of "unsafe" alias detection to defineAlias() The defineAlias() method in the Injector should carry the final responsibility of detecting "unsafe" aliases and prevent developer from defining such inside the target class. The Factory should perhaps to silently remove any properties from a concern class / aliases that fall under such category, to make it easier to use. --- packages/contracts/src/support/concerns/Factory.ts | 3 +-- packages/contracts/src/support/concerns/Injector.ts | 1 + packages/support/src/concerns/ConcernsInjector.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/Factory.ts b/packages/contracts/src/support/concerns/Factory.ts index e69a6c41..cb86a923 100644 --- a/packages/contracts/src/support/concerns/Factory.ts +++ b/packages/contracts/src/support/concerns/Factory.ts @@ -26,8 +26,7 @@ export default interface Factory * @param {ConcernConstructor | Configuration} entry * * @returns {Configuration} - * - * @throws {UnsafeAliasException} If an alias points to an "unsafe" property or method in concern + * * @throws {InjectionException} If entry is unsupported or invalid */ make(target: object, entry: ConcernConstructor | Configuration): Configuration; diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index 60b6bfb2..5fccfa46 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -112,6 +112,7 @@ export default interface Injector * @returns {boolean} `true` if "alias" was in target class. `false` if not, e.g. a property or method already * exists in target class' prototype chain, with the same name as the alias. * + * @throws {UnsafeAliasException} If an alias points to an "unsafe" property or method in concern * @throws {InjectionException} If unable to define "alias" in target class, e.g. due to failure when obtaining * or defining [property descriptors]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#description}. */ diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index a003a883..75669296 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -211,6 +211,7 @@ export default class ConcernsInjector implements Injector // * @returns {boolean} `true` if "alias" was in target class. `false` if not, e.g. a property or method already // * exists in target class' prototype chain, with the same name as the alias. // * + // * @throws {UnsafeAliasException} If an alias points to an "unsafe" property or method in concern // * @throws {InjectionException} If unable to define "alias" in target class, e.g. due to failure when obtaining // * or defining [property descriptors]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#description}. // */ From 19d58feb233efca7cccb4be96e337c537052ada3 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 18:38:07 +0100 Subject: [PATCH 321/424] Fix typo --- packages/contracts/src/support/concerns/Factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/Factory.ts b/packages/contracts/src/support/concerns/Factory.ts index cb86a923..0235a4b4 100644 --- a/packages/contracts/src/support/concerns/Factory.ts +++ b/packages/contracts/src/support/concerns/Factory.ts @@ -19,7 +19,7 @@ export default interface Factory * configuration is merged into the new configuration._ * * _**C**: Configuration's `aliases` are automatically populated. When a concern - * configuration is provided, its evt. aliases are merged with the default ones, + * configuration is provided, its evt. aliases merged with the default ones, * unless `allowAliases` is set to `false`, in which case all aliases are removed._ * * @param {object} target From 774a20330661003ecd68ae43bffb98d05eab359c Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 18:58:02 +0100 Subject: [PATCH 322/424] Add isConcernConfiguration() util function --- packages/support/src/concerns/index.ts | 1 + .../src/concerns/isConcernConfiguration.ts | 21 ++++++++ .../concerns/isConcernConfiguration.test.js | 48 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 packages/support/src/concerns/isConcernConfiguration.ts create mode 100644 tests/browser/packages/support/concerns/isConcernConfiguration.test.js diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index c03200ba..8f91cf46 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -11,5 +11,6 @@ export { }; export * from './exceptions'; +export * from './isConcernConfiguration'; export * from './isConcernConstructor'; export * from './use'; \ No newline at end of file diff --git a/packages/support/src/concerns/isConcernConfiguration.ts b/packages/support/src/concerns/isConcernConfiguration.ts new file mode 100644 index 00000000..0a881d09 --- /dev/null +++ b/packages/support/src/concerns/isConcernConfiguration.ts @@ -0,0 +1,21 @@ +import type { Configuration } from "@aedart/contracts/support/concerns"; +import { isConcernConstructor } from "./isConcernConstructor"; + +/** + * Determine if target a [Concern Configuration]{@link import('@aedart/contracts/support/concerns').Configuration} + * + * @param {object} target + * @param {boolean} [force=false] If `false` then cached result is returned if available. + * + * @returns {boolean} + */ +export function isConcernConfiguration(target: object, force: boolean = false): boolean +{ + // Note: A Concern Configuration only requires a `concern` property that + // must be a Concern Constructor. + + return typeof target == 'object' + && target !== null + && Reflect.has(target, 'concern') + && isConcernConstructor((target as Configuration).concern, force); +} \ No newline at end of file diff --git a/tests/browser/packages/support/concerns/isConcernConfiguration.test.js b/tests/browser/packages/support/concerns/isConcernConfiguration.test.js new file mode 100644 index 00000000..b0da9055 --- /dev/null +++ b/tests/browser/packages/support/concerns/isConcernConfiguration.test.js @@ -0,0 +1,48 @@ +import { PROVIDES } from "@aedart/contracts/support/concerns"; +import { isConcernConfiguration, AbstractConcern } from "@aedart/support/concerns"; + +describe('@aedart/support/concerns', () => { + describe('isConcernConfiguration()', () => { + + it('can determine if target is a concern configuration', () => { + + class A {} + + class B { + get concernOwner() { + return false; + } + + static [PROVIDES]() {} + } + + class C extends B {} + + class D extends AbstractConcern {} + + class E extends D {} + + // ------------------------------------------------------------------------------------ // + + const data = [ + { value: null, expected: false, name: 'Null' }, + { value: [], expected: false, name: 'Array' }, + { value: {}, expected: false, name: 'Object (empty)' }, + { value: A, expected: false, name: 'Class A (empty)' }, + { value: { concern: A }, expected: false, name: 'Configuration with Class A (not a concern)' }, + + { value: { concern: B }, expected: true, name: 'Configuration with Class B (custom implementation of concern)' }, + { value: { concern: C }, expected: true, name: 'Configuration with Class C (inherits from custom implementation)' }, + { value: { concern: D }, expected: true, name: 'Configuration with Class D (inherits from AbstractConcern)' }, + { value: { concern: E }, expected: true, name: 'Configuration with Class E (inherits from a base that inherits from AbstractConcern)' }, + ]; + + for (const entry of data) { + expect(isConcernConfiguration(entry.value)) + .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) + .toBe(entry.expected); + } + }); + + }); +}); \ No newline at end of file From c2a9f0a898693f164e4d08d99ad2c0d12c90344d Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 19:14:21 +0100 Subject: [PATCH 323/424] Change Aliases type, allow a default generic to be specified --- packages/contracts/src/support/concerns/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/types.ts b/packages/contracts/src/support/concerns/types.ts index 004cf0f5..a3a2d3af 100644 --- a/packages/contracts/src/support/concerns/types.ts +++ b/packages/contracts/src/support/concerns/types.ts @@ -13,6 +13,6 @@ export type Alias = PropertyKey; * class' prototype and acts as a proxy to the original property or method inside the * concern class instance. */ -export type Aliases = { +export type Aliases = { [K in keyof T]: Alias }; \ No newline at end of file From 2b0a9b7a9f54514500580e6c3f06054e9092869c Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 19:15:59 +0100 Subject: [PATCH 324/424] Add UNSAFE_PROPERTY_KEYS This is the implementation specific replacement for the HIDDEN const in concern contracts. --- packages/support/src/concerns/index.ts | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 8f91cf46..745d7a02 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -1,8 +1,48 @@ +import {DANGEROUS_PROPERTIES} from "@aedart/contracts/support/objects"; +import { + CONCERN_CLASSES, + CONCERNS, + PROVIDES +} from "@aedart/contracts/support/concerns"; + +/** + * List of "unsafe" property keys, which should NOT be aliased + * + * @type {PropertyKey[]} + */ +export const UNSAFE_PROPERTY_KEYS: PropertyKey[] = [ + ...DANGEROUS_PROPERTIES, + + // ----------------------------------------------------------------- // + // Defined by Concern interface / Abstract Concern: + // ----------------------------------------------------------------- // + + // It is NOT possible, nor advised to attempt to alias a Concern's + // constructor into a target class. + 'constructor', + + // The concernOwner property (getter) shouldn't be aliased either + 'concernOwner', + + // The static properties and methods (just in case...) + PROVIDES, + + // ----------------------------------------------------------------- // + // Other properties and methods: + // ----------------------------------------------------------------- // + + // In case that a concern class uses other concerns, prevent them + // from being aliased. + CONCERN_CLASSES, + CONCERNS, +] + import AbstractConcern from "./AbstractConcern"; import { ConcernClassBlueprint } from "./ConcernClassBlueprint"; import ConcernsContainer from "./ConcernsContainer"; import ConcernsInjector from "./ConcernsInjector"; + export { AbstractConcern, ConcernClassBlueprint, From be6c8a89e0cc1d73906654d4fbdbf81642b76b1d Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 19:20:37 +0100 Subject: [PATCH 325/424] Add isUnsafeKey() util function --- packages/support/src/concerns/index.ts | 1 + packages/support/src/concerns/isUnsafeKey.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 packages/support/src/concerns/isUnsafeKey.ts diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 745d7a02..5e464777 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -53,4 +53,5 @@ export { export * from './exceptions'; export * from './isConcernConfiguration'; export * from './isConcernConstructor'; +export * from './isUnsafeKey'; export * from './use'; \ No newline at end of file diff --git a/packages/support/src/concerns/isUnsafeKey.ts b/packages/support/src/concerns/isUnsafeKey.ts new file mode 100644 index 00000000..9833f574 --- /dev/null +++ b/packages/support/src/concerns/isUnsafeKey.ts @@ -0,0 +1,15 @@ +import { UNSAFE_PROPERTY_KEYS } from "@aedart/support/concerns/index"; + +/** + * Determine if given property key is considered "unsafe" + * + * @see UNSAFE_PROPERTY_KEYS + * + * @param {PropertyKey} key + * + * @returns {boolean} + */ +export function isUnsafeKey(key: PropertyKey): boolean +{ + return UNSAFE_PROPERTY_KEYS.includes(key); +} \ No newline at end of file From ce7b6d15b257036ccecd8dc01b109529f59d8d21 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 19:53:22 +0100 Subject: [PATCH 326/424] Refactor, move unsafe properties array into util method Had some trouble defining this due to some circular dependency. But, this seems to work just fine. --- packages/support/src/concerns/index.ts | 44 ++------------------ packages/support/src/concerns/isUnsafeKey.ts | 30 ++++++++++++- 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 5e464777..1b0f584e 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -1,53 +1,15 @@ -import {DANGEROUS_PROPERTIES} from "@aedart/contracts/support/objects"; -import { - CONCERN_CLASSES, - CONCERNS, - PROVIDES -} from "@aedart/contracts/support/concerns"; - -/** - * List of "unsafe" property keys, which should NOT be aliased - * - * @type {PropertyKey[]} - */ -export const UNSAFE_PROPERTY_KEYS: PropertyKey[] = [ - ...DANGEROUS_PROPERTIES, - - // ----------------------------------------------------------------- // - // Defined by Concern interface / Abstract Concern: - // ----------------------------------------------------------------- // - - // It is NOT possible, nor advised to attempt to alias a Concern's - // constructor into a target class. - 'constructor', - - // The concernOwner property (getter) shouldn't be aliased either - 'concernOwner', - - // The static properties and methods (just in case...) - PROVIDES, - - // ----------------------------------------------------------------- // - // Other properties and methods: - // ----------------------------------------------------------------- // - - // In case that a concern class uses other concerns, prevent them - // from being aliased. - CONCERN_CLASSES, - CONCERNS, -] - import AbstractConcern from "./AbstractConcern"; import { ConcernClassBlueprint } from "./ConcernClassBlueprint"; import ConcernsContainer from "./ConcernsContainer"; import ConcernsInjector from "./ConcernsInjector"; - +// import ConfigurationFactory from "./ConfigurationFactory"; export { AbstractConcern, ConcernClassBlueprint, ConcernsContainer, - ConcernsInjector + ConcernsInjector, + // ConfigurationFactory }; export * from './exceptions'; diff --git a/packages/support/src/concerns/isUnsafeKey.ts b/packages/support/src/concerns/isUnsafeKey.ts index 9833f574..910d09e2 100644 --- a/packages/support/src/concerns/isUnsafeKey.ts +++ b/packages/support/src/concerns/isUnsafeKey.ts @@ -1,4 +1,5 @@ -import { UNSAFE_PROPERTY_KEYS } from "@aedart/support/concerns/index"; +import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; +import { CONCERN_CLASSES, CONCERNS, PROVIDES } from "@aedart/contracts/support/concerns"; /** * Determine if given property key is considered "unsafe" @@ -11,5 +12,30 @@ import { UNSAFE_PROPERTY_KEYS } from "@aedart/support/concerns/index"; */ export function isUnsafeKey(key: PropertyKey): boolean { - return UNSAFE_PROPERTY_KEYS.includes(key); + return [ + ...DANGEROUS_PROPERTIES, + + // ----------------------------------------------------------------- // + // Defined by Concern interface / Abstract Concern: + // ----------------------------------------------------------------- // + + // It is NOT possible, nor advised to attempt to alias a Concern's + // constructor into a target class. + 'constructor', + + // The concernOwner property (getter) shouldn't be aliased either + 'concernOwner', + + // The static properties and methods (just in case...) + PROVIDES, + + // ----------------------------------------------------------------- // + // Other properties and methods: + // ----------------------------------------------------------------- // + + // In case that a concern class uses other concerns, prevent them + // from being aliased. + CONCERN_CLASSES, + CONCERNS, + ].includes(key); } \ No newline at end of file From 115e8f78b02606a1eb51b27abb6de0be07d19989 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 21:01:22 +0100 Subject: [PATCH 327/424] Add Concern Configuration Factory --- .../src/concerns/ConfigurationFactory.ts | 151 ++++++++++++++++++ packages/support/src/concerns/index.ts | 4 +- 2 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 packages/support/src/concerns/ConfigurationFactory.ts diff --git a/packages/support/src/concerns/ConfigurationFactory.ts b/packages/support/src/concerns/ConfigurationFactory.ts new file mode 100644 index 00000000..47855ca3 --- /dev/null +++ b/packages/support/src/concerns/ConfigurationFactory.ts @@ -0,0 +1,151 @@ +import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { + Aliases, + ConcernConstructor, + Configuration, + Factory +} from "@aedart/contracts/support/concerns"; +import { PROVIDES } from "@aedart/contracts/support/concerns"; +import { getNameOrDesc } from "@aedart/support/reflections"; +import { merge } from "@aedart/support/objects"; +import InjectionError from "./exceptions/InjectionError"; +import { isConcernConfiguration } from "./isConcernConfiguration"; +import { isConcernConstructor } from "./isConcernConstructor"; +import { isUnsafeKey } from "./isUnsafeKey"; + +/** + * Concern Configuration Factory + * + * @see Factory + */ +export default class ConfigurationFactory implements Factory +{ + /** + * Returns a new normalised concern configuration for given concern "entry" + * + * **Note**: _"normalised" in this context means:_ + * + * _**A**: If a concern class is given, then a new concern configuration made._ + * + * _**B**: If configuration is given, then a new concern configuration and given + * configuration is merged into the new configuration._ + * + * _**C**: Configuration's `aliases` are automatically populated. When a concern + * configuration is provided, its evt. aliases merged with the default ones, + * unless `allowAliases` is set to `false`, in which case all aliases are removed._ + * + * @param {object} target + * @param {ConcernConstructor | Configuration} entry + * + * @returns {Configuration} + * + * @throws {InjectionException} If entry is unsupported or invalid + */ + make(target: object, entry: ConcernConstructor | Configuration): Configuration { + // A) Make new configuration when concern class is given + if (isConcernConstructor(entry)) { + return this.makeConfiguration(entry as ConcernConstructor); + } + + // B) Make new configuration and merge provided configuration into it + if (isConcernConfiguration(entry)) { + // C) Merge given configuration with a new one, which has default aliases populated... + const configuration: Configuration = merge() + .using({ overwriteWithUndefined: false }) + .of( + this.makeConfiguration((entry as Configuration).concern), + entry + ) + + // Clear all aliases, if not allowed + if (!configuration.allowAliases) { + configuration.aliases = Object.create(null); + return configuration; + } + + // Otherwise, filter off evt. "unsafe" keys. + return this.removeUnsafeKeys(configuration); + } + + // Fail if entry is neither a concern class nor a concern configuration + const reason: string = `${getNameOrDesc(entry as ConstructorOrAbstractConstructor)} must be a valid Concern class or Concern Configuration` + throw new InjectionError(target as ConstructorOrAbstractConstructor, null, reason, { cause: { entry: entry } }); + } + + /** + * Returns a new concern configuration for the given concern class + * + * @param {ConcernConstructor} concern + * + * @returns {Configuration} + * + * @protected + */ + protected makeConfiguration(concern: ConcernConstructor): Configuration + { + return { + concern: concern, + aliases: this.makeDefaultAliases(concern), + allowAliases: true + } + } + + /** + * Returns the default aliases that are provided by the given concern class + * + * @param {ConcernConstructor} concern + * + * @returns {Aliases} + * + * @protected + */ + protected makeDefaultAliases(concern: ConcernConstructor): Aliases + { + const provides: PropertyKey[] = concern[PROVIDES]() + .filter((key: PropertyKey) => !this.isUnsafe(key)); + + const aliases: Aliases = Object.create(null); + for (const key of provides) { + // @ts-expect-error Type error is caused because we do not know the actual concern instance... + aliases[key] = key; + } + + return aliases; + } + + /** + * Removes evt. "unsafe" keys from configuration's aliases + * + * @param {Configuration} configuration + * + * @returns {Configuration} + * + * @protected + */ + protected removeUnsafeKeys(configuration: Configuration): Configuration + { + const keys: PropertyKey[] = Reflect.ownKeys(configuration.aliases as object); + for (const key of keys) { + if (this.isUnsafe(key)) { + // @ts-expect-error Property Key does exist at this point. + delete configuration.aliases[key]; + } + } + + return configuration; + } + + /** + * Determine if key is "unsafe" + * + * @param {PropertyKey} key + * + * @returns {boolean} + * + * @protected + */ + protected isUnsafe(key: PropertyKey): boolean + { + return isUnsafeKey(key); + } +} \ No newline at end of file diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 1b0f584e..2d78e0f9 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -2,14 +2,14 @@ import AbstractConcern from "./AbstractConcern"; import { ConcernClassBlueprint } from "./ConcernClassBlueprint"; import ConcernsContainer from "./ConcernsContainer"; import ConcernsInjector from "./ConcernsInjector"; -// import ConfigurationFactory from "./ConfigurationFactory"; +import ConfigurationFactory from "./ConfigurationFactory"; export { AbstractConcern, ConcernClassBlueprint, ConcernsContainer, ConcernsInjector, - // ConfigurationFactory + ConfigurationFactory }; export * from './exceptions'; From 51e966549b6a45edfc03ef704dd367802a52d02b Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 21:01:32 +0100 Subject: [PATCH 328/424] Add tests for configuration factory --- .../concerns/ConfigurationFactory.test.js | 217 ++++++++++++++++++ .../helpers/makeConfigurationFactory.js | 11 + 2 files changed, 228 insertions(+) create mode 100644 tests/browser/packages/support/concerns/ConfigurationFactory.test.js create mode 100644 tests/browser/packages/support/concerns/helpers/makeConfigurationFactory.js diff --git a/tests/browser/packages/support/concerns/ConfigurationFactory.test.js b/tests/browser/packages/support/concerns/ConfigurationFactory.test.js new file mode 100644 index 00000000..20a10d95 --- /dev/null +++ b/tests/browser/packages/support/concerns/ConfigurationFactory.test.js @@ -0,0 +1,217 @@ +import makeConfigurationFactory from "./helpers/makeConfigurationFactory"; +import {AbstractConcern, InjectionError, isUnsafeKey} from "@aedart/support/concerns"; + +describe('@aedart/support/concerns', () => { + describe('ConfigurationFactory', () => { + + it('fails when entry is neither a concern class or configuration', () => { + + const entry = {}; + + const factory = makeConfigurationFactory(); + + // ------------------------------------------------------------------ // + + const callback = () => factory.make(class {}, entry); + + expect(callback) + .toThrowError(InjectionError); + }); + + it('returns default configuration for a concern class', () => { + + class MyConcern extends AbstractConcern { + foo() {} + } + + const factory = makeConfigurationFactory(); + + // ------------------------------------------------------------------ // + + const configuration = factory.make(class {}, MyConcern); + + // Debug + // console.log('configuration', configuration); + + // concern + expect(Reflect.has(configuration, 'concern')) + .withContext('Concern property does not exist in configuration') + .toBeTrue(); + expect(configuration.concern) + .withContext('Incorrect concern class in configuration') + .toBe(MyConcern); + + // aliases + expect(Reflect.has(configuration, 'aliases')) + .withContext('Aliases property does not exist in configuration') + .toBeTrue(); + expect(configuration.aliases) + .withContext('Incorrect aliases object in configuration') + .toEqual({ 'foo': 'foo' }); + + // allowAliases + expect(Reflect.has(configuration, 'allowAliases')) + .withContext('AllowAliases property does not exist in configuration') + .toBeTrue(); + expect(configuration.allowAliases) + .withContext('Incorrect allowAliases value configuration') + .toBeTrue(); + }); + + it('populates aliases when not given in configuration', () => { + class MyConcern extends AbstractConcern { + zoom() {} + } + + const entry = { concern: MyConcern } + + const factory = makeConfigurationFactory(); + + // ------------------------------------------------------------------ // + + const configuration = factory.make(class {}, entry); + + // Debug + // console.log('configuration', configuration); + + // concern + expect(Reflect.has(configuration, 'concern')) + .withContext('Concern property does not exist in configuration') + .toBeTrue(); + expect(configuration.concern) + .withContext('Incorrect concern class in configuration') + .toBe(MyConcern); + + // aliases + expect(Reflect.has(configuration, 'aliases')) + .withContext('Aliases property does not exist in configuration') + .toBeTrue(); + expect(configuration.aliases) + .withContext('Incorrect aliases object in configuration') + .toEqual({ 'zoom': 'zoom' }); + + // allowAliases + expect(Reflect.has(configuration, 'allowAliases')) + .withContext('AllowAliases property does not exist in configuration') + .toBeTrue(); + expect(configuration.allowAliases) + .withContext('Incorrect allowAliases value configuration') + .toBeTrue(); + }); + + it('can specify custom aliases', () => { + class MyConcern extends AbstractConcern { + foo() {} + + bar() {} + } + + const aliases = { + 'foo': 'a', + 'bar': 'b' + }; + const entry = { concern: MyConcern, aliases: aliases }; + + const factory = makeConfigurationFactory(); + + // ------------------------------------------------------------------ // + + const configuration = factory.make(class {}, entry); + + // Debug + // console.log('configuration', configuration); + + expect(configuration.aliases) + .withContext('Incorrect aliases object in configuration') + .toEqual(aliases); + }); + + it('merges default aliases with custom aliases', () => { + class MyConcern extends AbstractConcern { + foo() {} + + bar() {} + } + + const aliases = { + 'bar': 'a' + }; + const entry = { concern: MyConcern, aliases: aliases }; + + const factory = makeConfigurationFactory(); + + // ------------------------------------------------------------------ // + + const configuration = factory.make(class {}, entry); + + // Debug + // console.log('configuration', configuration); + + expect(configuration.aliases) + .withContext('Incorrect aliases object in configuration') + .toEqual({ 'foo': 'foo', ...aliases }); + }); + + it('can disable aliases', () => { + class MyConcern extends AbstractConcern { + sayGoodBye() {} + + sayHi() {} + } + + + const entry = { concern: MyConcern, allowAliases: false }; + + const factory = makeConfigurationFactory(); + + // ------------------------------------------------------------------ // + + const configuration = factory.make(class {}, entry); + + // Debug + // console.log('configuration', configuration); + + expect(configuration.allowAliases) + .withContext('Incorrect allowAliases value configuration') + .toBeFalse(); + + expect(configuration.aliases) + .withContext('Incorrect aliases object in configuration') + .toEqual({}); + }); + + it('removes unsafe keys / aliases', () => { + class MyConcern extends AbstractConcern { + foo() {} + } + + const aliases = { + 'foo': 'a', + + 'constructor': 'danger' // This should NOT be part of normalised aliases + }; + const entry = { concern: MyConcern, aliases: aliases }; + + const factory = makeConfigurationFactory(); + + // ------------------------------------------------------------------ // + + const configuration = factory.make(class {}, entry); + + // Debug + // console.log('configuration', configuration); + + expect(configuration.aliases) + .withContext('Incorrect aliases object in configuration') + .not + .toEqual({ 'foo': 'a', ...aliases }); + + const keys = Reflect.ownKeys(configuration.aliases); + for (const key of keys) { + expect(isUnsafeKey(key)) + .withContext(`Unsafe key "${key.toString()}" is part of aliases`) + .toBeFalse(); + } + }); + }); +}); \ No newline at end of file diff --git a/tests/browser/packages/support/concerns/helpers/makeConfigurationFactory.js b/tests/browser/packages/support/concerns/helpers/makeConfigurationFactory.js new file mode 100644 index 00000000..80dbce98 --- /dev/null +++ b/tests/browser/packages/support/concerns/helpers/makeConfigurationFactory.js @@ -0,0 +1,11 @@ +import { ConfigurationFactory } from "@aedart/support/concerns"; + +/** + * Returns a new concern configuration factory instance + * + * @returns {ConfigurationFactory} + */ +export default function makeConfigurationFactory() +{ + return new ConfigurationFactory(); +} \ No newline at end of file From f5187a47bb72164b401aa3e276951d36ad4e54c9 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 21:13:00 +0100 Subject: [PATCH 329/424] Use configuration factory in normalise util Also cleaned up... --- .../support/src/concerns/ConcernsInjector.ts | 105 ++++++------------ 1 file changed, 33 insertions(+), 72 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 75669296..fe0608c2 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -1,15 +1,14 @@ import type { - Concern, ConcernConstructor, Injector, MustUseConcerns, Configuration, Owner, Container, + Factory, } from "@aedart/contracts/support/concerns"; import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import { - ALWAYS_HIDDEN, CONCERN_CLASSES, CONCERNS } from "@aedart/contracts/support/concerns"; @@ -22,6 +21,7 @@ import { InjectionError } from "./exceptions"; import ConcernsContainer from './ConcernsContainer'; +import ConfigurationFactory from "./ConfigurationFactory"; /** * A map of the concern owner instances and their concerns container @@ -51,6 +51,15 @@ export default class ConcernsInjector implements Injector */ readonly #target: T; + /** + * Concern Configuration Factory + * + * @type {Factory} + * + * @protected + */ + protected factory: Factory; + /** * Create a new Concerns Injector instance * @@ -61,6 +70,7 @@ export default class ConcernsInjector implements Injector public constructor(target: T) { this.#target = target; + this.factory = this.makeConfigurationFactory(); } /** @@ -241,20 +251,12 @@ export default class ConcernsInjector implements Injector const output: Configuration[] = []; for (const entry of concerns) { - output.push(this.resolveConfiguration(entry)); + output.push(this.normaliseEntry(entry)); } return output; } - // TODO: Incomplete - protected resolveConfiguration(entry: ConcernConstructor|Configuration): Configuration - { - // TODO: Use Concern Configuration Factory to resolve configuration - - return {} as Configuration; - } - /***************************************************************** * Internals ****************************************************************/ @@ -296,6 +298,22 @@ export default class ConcernsInjector implements Injector return registry; } + + /** + * Normalises the given entry into a concern configuration + * + * @param {ConcernConstructor | Configuration} entry + * + * @returns {Configuration} + * + * @throws {InjectionError} + * + * @protected + */ + protected normaliseEntry(entry: ConcernConstructor|Configuration): Configuration + { + return this.factory.make(this.target as object, entry); + } /** * Defines a property in given target @@ -355,72 +373,15 @@ export default class ConcernsInjector implements Injector return null; } - // ------------------------------------------------------------------------- // - // TODO: Was previously part of AbstractConcern, but the logic should belong here - // TODO: instead... - /** - * TODO: Adapt this... + * Returns a new concern configuration factory instance * - * In-memory cache of resolved keys (properties and methods), which - * are offered by concern(s) and can be aliased. - * - * @type {WeakMap, PropertyKey[]>} - * - * @protected - * @static - */ - protected static resolvedConcernKeys: WeakMap, PropertyKey[]> = new WeakMap(); - - /** - * TODO: Adapt this... + * @returns {Factory} * - * Removes keys that should remain hidden - * - * @see ALWAYS_HIDDEN - * - * @param {PropertyKey[]} keys - * - * @returns {PropertyKey[]} - * * @protected - * @static */ - protected static removeAlwaysHiddenKeys(keys: PropertyKey[]): PropertyKey[] + protected makeConfigurationFactory(): Factory { - return keys.filter((key: PropertyKey) => { - return !ALWAYS_HIDDEN.includes(key); - }); - } - - /** - * TODO: Adapt this... - * - * Remember the resolved keys (properties and methods) for given target concern class - * - * @param {ThisType} concern - * @param {() => PropertyKey[]} callback - * @param {boolean} [force=false] - * - * @returns {PropertyKey[]} - * - * @protected - * @static - */ - protected static rememberConcernKeys( - concern: ThisType, - callback: () => PropertyKey[], - force: boolean = false - ): PropertyKey[] - { - if (!force && this.resolvedConcernKeys.has(concern)) { - return this.resolvedConcernKeys.get(concern) as PropertyKey[]; - } - - const keys: PropertyKey[] = callback(); - - this.resolvedConcernKeys.set(concern, keys); - - return keys; + return new ConfigurationFactory(); } } \ No newline at end of file From cf15261d480801985b4652885f3ef77f3b893d54 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 21:17:16 +0100 Subject: [PATCH 330/424] (Re)add UNSAFE_PROPERTY_KEYS const Normally, this could be inside the index.ts file, but this works too. --- packages/support/src/concerns/isUnsafeKey.ts | 57 +++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/packages/support/src/concerns/isUnsafeKey.ts b/packages/support/src/concerns/isUnsafeKey.ts index 910d09e2..e7dd12fc 100644 --- a/packages/support/src/concerns/isUnsafeKey.ts +++ b/packages/support/src/concerns/isUnsafeKey.ts @@ -1,6 +1,36 @@ import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; import { CONCERN_CLASSES, CONCERNS, PROVIDES } from "@aedart/contracts/support/concerns"; +/** + * List of property keys that are considered "unsafe" to alias (proxy to) + */ +export const UNSAFE_PROPERTY_KEYS = [ + ...DANGEROUS_PROPERTIES, + + // ----------------------------------------------------------------- // + // Defined by Concern interface / Abstract Concern: + // ----------------------------------------------------------------- // + + // It is NOT possible, nor advised to attempt to alias a Concern's + // constructor into a target class. + 'constructor', + + // The concernOwner property (getter) shouldn't be aliased either + 'concernOwner', + + // The static properties and methods (just in case...) + PROVIDES, + + // ----------------------------------------------------------------- // + // Other properties and methods: + // ----------------------------------------------------------------- // + + // In case that a concern class uses other concerns, prevent them + // from being aliased. + CONCERN_CLASSES, + CONCERNS, +]; + /** * Determine if given property key is considered "unsafe" * @@ -12,30 +42,5 @@ import { CONCERN_CLASSES, CONCERNS, PROVIDES } from "@aedart/contracts/support/c */ export function isUnsafeKey(key: PropertyKey): boolean { - return [ - ...DANGEROUS_PROPERTIES, - - // ----------------------------------------------------------------- // - // Defined by Concern interface / Abstract Concern: - // ----------------------------------------------------------------- // - - // It is NOT possible, nor advised to attempt to alias a Concern's - // constructor into a target class. - 'constructor', - - // The concernOwner property (getter) shouldn't be aliased either - 'concernOwner', - - // The static properties and methods (just in case...) - PROVIDES, - - // ----------------------------------------------------------------- // - // Other properties and methods: - // ----------------------------------------------------------------- // - - // In case that a concern class uses other concerns, prevent them - // from being aliased. - CONCERN_CLASSES, - CONCERNS, - ].includes(key); + return UNSAFE_PROPERTY_KEYS.includes(key); } \ No newline at end of file From a98c24cdd54675b0dba82a9ac5f951b6925e2d95 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 24 Feb 2024 21:22:57 +0100 Subject: [PATCH 331/424] Remove deprecated HIDDEN and ALWAYS_HIDDEN Have now been replaced by PROVIDES and UNSAFE_PROPERTY_KEYS. --- .../contracts/src/support/concerns/index.ts | 70 ------------------- 1 file changed, 70 deletions(-) diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 0db5cd7d..d0919204 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -1,5 +1,3 @@ -import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; - /** * Support Concerns identifier * @@ -7,31 +5,6 @@ import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; */ export const SUPPORT_CONCERNS: unique symbol = Symbol('@aedart/contracts/support/concerns'); -/** - * @deprecated TODO: This MUST be redesigned, such that each Concern class can provide a list of what to expose - * - * Symbol used by a {@link Concern} to define properties or methods that must be - * "hidden" and not allowed to be aliased into a target class. - * - * **Note**: _Symbol MUST be used to as name for a "static" method in the desired Concern class._ - * - * **Example**: - * ```ts - * class MyConcern implements Concern - * { - * static [HIDDEN](): PropertyKey[] - * { - * // ...not shown... - * } - * - * // ...remaining not shown... - * } - * ``` - * - * @type {symbol} - */ -export const HIDDEN: unique symbol = Symbol('hidden'); - /** * Symbol used by a [concern class]{@link ConcernConstructor} to indicate what properties * and methods can be aliased into a target class. @@ -74,48 +47,6 @@ export const CONCERN_CLASSES: unique symbol = Symbol('concern_classes'); */ export const CONCERNS: unique symbol = Symbol('concerns'); -/** - * @deprecated TODO: Move this into support/concerns. It is way too implementation specific to belong here. - * - * List of properties and methods that must always remain "hidden" and - * **NEVER** be aliased into a target class' prototype. - * - * @type {ReadonlyArray} - */ -export const ALWAYS_HIDDEN: ReadonlyArray = [ - - ...DANGEROUS_PROPERTIES, - - // ----------------------------------------------------------------- // - // Defined by Concern interface / Abstract Concern: - // ----------------------------------------------------------------- // - - // It is NOT possible, nor advised to attempt to alias a Concern's - // constructor into a target class. - 'constructor', - - // The concernOwner property (getter) shouldn't be aliased either - 'concernOwner', - - // If the Concern defines any hidden properties or methods, - // then such a method will not do any good in a target class. - HIDDEN, - - // The static properties and methods (just in case...) - PROVIDES, - 'resolvedConcernKeys', - 'removeAlwaysHiddenKeys', - 'rememberConcernKeys', - - // ----------------------------------------------------------------- // - // Other properties and methods: - // ----------------------------------------------------------------- // - - // In case that a concern class uses other concerns, prevent them - // from being aliased. - CONCERNS, -]; - import Concern from "./Concern"; import ConcernConstructor from "./ConcernConstructor"; import Configuration from "./Configuration"; @@ -136,5 +67,4 @@ export { } export * from './exceptions/index'; - export * from './types'; \ No newline at end of file From 1d8e193a8cf3b0cc5fb9eb571d1f5c02cf1f5113 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 25 Feb 2024 13:11:58 +0100 Subject: [PATCH 332/424] Fix CI fails date comparison --- tests/browser/packages/support/objects/merge.test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/browser/packages/support/objects/merge.test.js b/tests/browser/packages/support/objects/merge.test.js index 012e3b30..0af3fcba 100644 --- a/tests/browser/packages/support/objects/merge.test.js +++ b/tests/browser/packages/support/objects/merge.test.js @@ -672,6 +672,9 @@ describe('@aedart/support/objects', () => { }); it('can clones objects of native kind', () => { + + const now = new Date(); + const dataSet = [ { name: 'ArrayBuffer', @@ -699,10 +702,10 @@ describe('@aedart/support/objects', () => { }, { name: 'Date', - source: { value: new Date('2024-02-19 14:13:25') }, + source: { value: now }, expectedInstanceOf: Date, match: (cloned) => { - return cloned.valueOf() === 1708348405000; // milliseconds for since the epoch for date + return cloned.valueOf() === now.valueOf(); // milliseconds for since the epoch for date } }, { From ab1f0a7b959cae7f6fc7228d1957ac6060668759 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 25 Feb 2024 13:18:28 +0100 Subject: [PATCH 333/424] Rename MustUseConcerns to UsesConcerns --- .../src/support/concerns/Injector.ts | 28 ++++++++-------- .../{MustUseConcerns.ts => UsesConcerns.ts} | 8 ++--- .../exceptions/AlreadyRegisteredException.ts | 6 ++-- .../concerns/exceptions/InjectionException.ts | 6 ++-- .../contracts/src/support/concerns/index.ts | 8 ++--- .../support/src/concerns/ConcernsInjector.ts | 32 +++++++++---------- .../exceptions/AlreadyRegisteredError.ts | 14 ++++---- .../src/concerns/exceptions/InjectionError.ts | 14 ++++---- .../concerns/exceptions/UnsafeAliasError.ts | 6 ++-- packages/support/src/concerns/use.ts | 2 +- 10 files changed, 61 insertions(+), 63 deletions(-) rename packages/contracts/src/support/concerns/{MustUseConcerns.ts => UsesConcerns.ts} (80%) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index 5fccfa46..15324ab6 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -1,7 +1,7 @@ import Concern from "./Concern"; import ConcernConstructor from "./ConcernConstructor"; import Configuration from "./Configuration"; -import MustUseConcerns from "./MustUseConcerns"; +import UsesConcerns from "./UsesConcerns"; /** * Concerns Injector @@ -37,29 +37,29 @@ export default interface Injector * * @param {ConcernConstructor | Configuration} concerns List of concern classes / injection configurations * - * @returns {MustUseConcerns} The modified target class + * @returns {UsesConcerns} The modified target class * * @throws {InjectionException} */ - inject(...concerns: (ConcernConstructor|Configuration)[]): MustUseConcerns; + inject(...concerns: (ConcernConstructor|Configuration)[]): UsesConcerns; /** * Defines the concern classes that must be used by the target class. * * **Note**: _Method changes the target class, such that it implements and respects the - * {@link MustUseConcerns} interface._ + * {@link UsesConcerns} interface._ * * @template T = object * * @param {T} target The target class that must define the concern classes to be used * @param {Constructor[]} concerns List of concern classes * - * @returns {MustUseConcerns} The modified target class + * @returns {UsesConcerns} The modified target class * * @throws {AlreadyRegisteredException} * @throws {InjectionException} */ - defineConcerns(target: T, concerns: ConcernConstructor[]): MustUseConcerns; + defineConcerns(target: T, concerns: ConcernConstructor[]): UsesConcerns; /** * Defines a concerns {@link Container} in target class' prototype. @@ -69,13 +69,13 @@ export default interface Injector * * @template T = object * - * @param {MustUseConcerns} target The target in which a concerns container must be defined + * @param {UsesConcerns} target The target in which a concerns container must be defined * - * @returns {MustUseConcerns} The modified target class + * @returns {UsesConcerns} The modified target class * * @throws {InjectionException} If unable to define concerns container in target class */ - defineContainer(target: MustUseConcerns): MustUseConcerns; + defineContainer(target: UsesConcerns): UsesConcerns; /** * Defines "aliases" (proxy properties and methods) in target class' prototype, such that they @@ -85,14 +85,14 @@ export default interface Injector * * @template T = object * - * @param {MustUseConcerns} target The target in which "aliases" must be defined in + * @param {UsesConcerns} target The target in which "aliases" must be defined in * @param {Configuration[]} configurations List of concern injection configurations * - * @returns {MustUseConcerns} The modified target class + * @returns {UsesConcerns} The modified target class * * @throws {InjectionException} If case of alias naming conflicts. Or, if unable to define aliases in target class. */ - defineAliases(target: MustUseConcerns, configurations: Configuration[]): MustUseConcerns; + defineAliases(target: UsesConcerns, configurations: Configuration[]): UsesConcerns; /** * Defines an "alias" (proxy property or method) in target class' prototype, to a property or method @@ -104,7 +104,7 @@ export default interface Injector * @template C extends Concern * @template T = object * - * @param {MustUseConcerns} target The target in which "alias" must be defined in + * @param {UsesConcerns} target The target in which "alias" must be defined in * @param {PropertyKey} alias Name of the "alias" in the target class (name of the proxy property or method) * @param {PropertyKey} key Name of the property or method that the "alias" is for, in the concern class (`source`) * @param {Constructor} source The concern class that holds the property or methods (`key`) @@ -117,7 +117,7 @@ export default interface Injector * or defining [property descriptors]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#description}. */ defineAlias( - target: MustUseConcerns, + target: UsesConcerns, alias: PropertyKey, key: PropertyKey, source: ConcernConstructor diff --git a/packages/contracts/src/support/concerns/MustUseConcerns.ts b/packages/contracts/src/support/concerns/UsesConcerns.ts similarity index 80% rename from packages/contracts/src/support/concerns/MustUseConcerns.ts rename to packages/contracts/src/support/concerns/UsesConcerns.ts index c740e02b..97f375c8 100644 --- a/packages/contracts/src/support/concerns/MustUseConcerns.ts +++ b/packages/contracts/src/support/concerns/UsesConcerns.ts @@ -4,15 +4,13 @@ import ConcernConstructor from "./ConcernConstructor"; import Owner from "./Owner"; /** - * Must Use Concerns + * Uses Concerns * - * Defines a list of concern classes that this class instance must use. - * - * **Note**: _The herein defined properties and methods MUST be implemented as static_ + * A target class that uses one or more concern classes. * * @template T = object */ -export default interface MustUseConcerns +export default interface UsesConcerns { /** * Constructor diff --git a/packages/contracts/src/support/concerns/exceptions/AlreadyRegisteredException.ts b/packages/contracts/src/support/concerns/exceptions/AlreadyRegisteredException.ts index 0b75090e..a514b947 100644 --- a/packages/contracts/src/support/concerns/exceptions/AlreadyRegisteredException.ts +++ b/packages/contracts/src/support/concerns/exceptions/AlreadyRegisteredException.ts @@ -1,6 +1,6 @@ import { InjectionException } from "@aedart/contracts/support/concerns"; import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; -import MustUseConcerns from "../MustUseConcerns"; +import UsesConcerns from "../UsesConcerns"; /** * Already Registered Exception @@ -16,7 +16,7 @@ export default interface AlreadyRegisteredException extends InjectionException * * @readonly * - * @type {ConstructorOrAbstractConstructor|MustUseConcerns} + * @type {ConstructorOrAbstractConstructor|UsesConcerns} */ - readonly source: ConstructorOrAbstractConstructor | MustUseConcerns; + readonly source: ConstructorOrAbstractConstructor | UsesConcerns; } \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/exceptions/InjectionException.ts b/packages/contracts/src/support/concerns/exceptions/InjectionException.ts index e31e2dda..a2bac41b 100644 --- a/packages/contracts/src/support/concerns/exceptions/InjectionException.ts +++ b/packages/contracts/src/support/concerns/exceptions/InjectionException.ts @@ -1,6 +1,6 @@ import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import ConcernException from "./ConcernException"; -import MustUseConcerns from "../MustUseConcerns"; +import UsesConcerns from "../UsesConcerns"; /** * Concern Injection Exception @@ -14,7 +14,7 @@ export default interface InjectionException extends ConcernException * * @readonly * - * @type {ConstructorOrAbstractConstructor|MustUseConcerns} + * @type {ConstructorOrAbstractConstructor|UsesConcerns} */ - readonly target: ConstructorOrAbstractConstructor | MustUseConcerns; + readonly target: ConstructorOrAbstractConstructor | UsesConcerns; } \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index d0919204..092823cf 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -31,7 +31,7 @@ export const PROVIDES : unique symbol = Symbol('concern_provides'); /** * Symbol used to define a list of the concern classes to be used by a target class. * - * @see {MustUseConcerns} + * @see {UsesConcerns} * * @type {Symbol} */ @@ -52,9 +52,9 @@ import ConcernConstructor from "./ConcernConstructor"; import Configuration from "./Configuration"; import Container from "./Container"; import Factory from "./Factory"; -import MustUseConcerns from "./MustUseConcerns"; import Injector from "./Injector"; import Owner from "./Owner"; +import UsesConcerns from "./UsesConcerns"; export { type Concern, type ConcernConstructor, @@ -62,8 +62,8 @@ export { type Container, type Factory, type Injector, - type MustUseConcerns, - type Owner + type Owner, + type UsesConcerns } export * from './exceptions/index'; diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index fe0608c2..d3099c89 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -1,7 +1,7 @@ import type { ConcernConstructor, Injector, - MustUseConcerns, + UsesConcerns, Configuration, Owner, Container, @@ -102,11 +102,11 @@ export default class ConcernsInjector implements Injector // * // * @param {ConcernConstructor | Configuration} concerns List of concern classes / injection configurations // * - // * @returns {MustUseConcerns} The modified target class + // * @returns {UsesConcerns} The modified target class // * // * @throws {InjectionException} // */ - // inject(...concerns: (ConcernConstructor|Configuration)[]): MustUseConcerns; + // inject(...concerns: (ConcernConstructor|Configuration)[]): UsesConcerns; // { // // TODO: implement this method... // @@ -118,26 +118,26 @@ export default class ConcernsInjector implements Injector // // // C) Define "aliases" (proxy properties and methods) in target class' prototype // - // return this.target as MustUseConcerns; + // return this.target as UsesConcerns; // } /** * Defines the concern classes that must be used by the target class. * * **Note**: _Method changes the target class, such that it implements and respects the - * {@link MustUseConcerns} interface._ + * {@link UsesConcerns} interface._ * * @template T = object * * @param {T} target The target class that must define the concern classes to be used * @param {Constructor[]} concerns List of concern classes * - * @returns {MustUseConcerns} The modified target class + * @returns {UsesConcerns} The modified target class * * @throws {AlreadyRegisteredError} * @throws {InjectionError} */ - public defineConcerns(target: T, concerns: ConcernConstructor[]): MustUseConcerns + public defineConcerns(target: T, concerns: ConcernConstructor[]): UsesConcerns { const registry = this.resolveConcernsRegistry(target as object, concerns); @@ -145,7 +145,7 @@ export default class ConcernsInjector implements Injector get: function() { return registry; } - }) as MustUseConcerns; + }) as UsesConcerns; } /** @@ -156,13 +156,13 @@ export default class ConcernsInjector implements Injector * * @template T = object * - * @param {MustUseConcerns} target The target in which a concerns container must be defined + * @param {UsesConcerns} target The target in which a concerns container must be defined * - * @returns {MustUseConcerns} The modified target class + * @returns {UsesConcerns} The modified target class * * @throws {InjectionError} If unable to define concerns container in target class */ - public defineContainer(target: MustUseConcerns): MustUseConcerns + public defineContainer(target: UsesConcerns): UsesConcerns { const concerns: ConcernConstructor[] = target[CONCERN_CLASSES]; @@ -190,14 +190,14 @@ export default class ConcernsInjector implements Injector // * // * @template T = object // * - // * @param {MustUseConcerns} target The target in which "aliases" must be defined in + // * @param {UsesConcerns} target The target in which "aliases" must be defined in // * @param {Configuration[]} configurations List of concern injection configurations // * - // * @returns {MustUseConcerns} The modified target class + // * @returns {UsesConcerns} The modified target class // * // * @throws {InjectionException} If case of alias naming conflicts. Or, if unable to define aliases in target class. // */ - // public defineAliases(target: MustUseConcerns, configurations: Configuration[]): MustUseConcerns + // public defineAliases(target: UsesConcerns, configurations: Configuration[]): UsesConcerns // { // // TODO: implement this method... // @@ -213,7 +213,7 @@ export default class ConcernsInjector implements Injector // * // * @template T = object // * - // * @param {MustUseConcerns} target The target in which "alias" must be defined in + // * @param {UsesConcerns} target The target in which "alias" must be defined in // * @param {PropertyKey} alias Name of the "alias" in the target class (name of the proxy property or method) // * @param {PropertyKey} key Name of the property or method that the "alias" is for, in the concern class (`source`) // * @param {Constructor} source The concern class that holds the property or methods (`key`) @@ -226,7 +226,7 @@ export default class ConcernsInjector implements Injector // * or defining [property descriptors]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#description}. // */ // public defineAlias( - // target: MustUseConcerns, + // target: UsesConcerns, // alias: PropertyKey, // key: PropertyKey, // source: Constructor diff --git a/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts b/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts index 52399e22..28d465e8 100644 --- a/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts +++ b/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts @@ -1,7 +1,7 @@ import type { AlreadyRegisteredException, ConcernConstructor, - MustUseConcerns + UsesConcerns } from "@aedart/contracts/support/concerns"; import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import { configureCustomError } from "@aedart/support/exceptions"; @@ -22,14 +22,14 @@ export default class AlreadyRegisteredError extends InjectionError implements Al * @readonly * @private * - * @type {ConstructorOrAbstractConstructor|MustUseConcerns} + * @type {ConstructorOrAbstractConstructor|UsesConcerns} */ - readonly #source: ConstructorOrAbstractConstructor | MustUseConcerns; + readonly #source: ConstructorOrAbstractConstructor | UsesConcerns; constructor( - target: ConstructorOrAbstractConstructor | MustUseConcerns, + target: ConstructorOrAbstractConstructor | UsesConcerns, concern: ConcernConstructor, - source: ConstructorOrAbstractConstructor | MustUseConcerns, + source: ConstructorOrAbstractConstructor | UsesConcerns, message?: string, options?: ErrorOptions ) { @@ -53,9 +53,9 @@ export default class AlreadyRegisteredError extends InjectionError implements Al * * @readonly * - * @returns {ConstructorOrAbstractConstructor | MustUseConcerns} + * @returns {ConstructorOrAbstractConstructor | UsesConcerns} */ - get source(): ConstructorOrAbstractConstructor | MustUseConcerns + get source(): ConstructorOrAbstractConstructor | UsesConcerns { return this.#source; } diff --git a/packages/support/src/concerns/exceptions/InjectionError.ts b/packages/support/src/concerns/exceptions/InjectionError.ts index d0038129..e5c31436 100644 --- a/packages/support/src/concerns/exceptions/InjectionError.ts +++ b/packages/support/src/concerns/exceptions/InjectionError.ts @@ -1,4 +1,4 @@ -import type { ConcernConstructor, InjectionException, MustUseConcerns } from "@aedart/contracts/support/concerns"; +import type { ConcernConstructor, InjectionException, UsesConcerns } from "@aedart/contracts/support/concerns"; import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import { configureCustomError } from "@aedart/support/exceptions"; import ConcernError from "./ConcernError"; @@ -15,20 +15,20 @@ export default class InjectionError extends ConcernError implements InjectionExc * * @readonly * - * @type {ConstructorOrAbstractConstructor|MustUseConcerns} + * @type {ConstructorOrAbstractConstructor|UsesConcerns} */ - readonly #target: ConstructorOrAbstractConstructor | MustUseConcerns; + readonly #target: ConstructorOrAbstractConstructor | UsesConcerns; /** * Create a new Injection Error instance * - * @param {ConstructorOrAbstractConstructor | MustUseConcerns} target + * @param {ConstructorOrAbstractConstructor | UsesConcerns} target * @param {ConcernConstructor | null} concern * @param {string} message * @param {ErrorOptions} [options] */ constructor( - target: ConstructorOrAbstractConstructor | MustUseConcerns, + target: ConstructorOrAbstractConstructor | UsesConcerns, concern: ConcernConstructor | null, message: string, options?: ErrorOptions @@ -48,9 +48,9 @@ export default class InjectionError extends ConcernError implements InjectionExc * * @readonly * - * @returns {ConstructorOrAbstractConstructor | MustUseConcerns} + * @returns {ConstructorOrAbstractConstructor | UsesConcerns} */ - get target(): ConstructorOrAbstractConstructor | MustUseConcerns + get target(): ConstructorOrAbstractConstructor | UsesConcerns { return this.#target; } diff --git a/packages/support/src/concerns/exceptions/UnsafeAliasError.ts b/packages/support/src/concerns/exceptions/UnsafeAliasError.ts index 80881b96..02cfc8b4 100644 --- a/packages/support/src/concerns/exceptions/UnsafeAliasError.ts +++ b/packages/support/src/concerns/exceptions/UnsafeAliasError.ts @@ -1,6 +1,6 @@ import type { ConcernConstructor, - MustUseConcerns, UnsafeAliasException + UsesConcerns, UnsafeAliasException } from "@aedart/contracts/support/concerns"; import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import { configureCustomError } from "@aedart/support/exceptions"; @@ -33,7 +33,7 @@ export default class UnsafeAliasError extends InjectionError implements UnsafeAl /** * Create a new Unsafe Alias Error instance * - * @param {ConstructorOrAbstractConstructor | MustUseConcerns} target + * @param {ConstructorOrAbstractConstructor | UsesConcerns} target * @param {ConcernConstructor} concern * @param {PropertyKey} alias * @param {PropertyKey} key @@ -41,7 +41,7 @@ export default class UnsafeAliasError extends InjectionError implements UnsafeAl * @param {ErrorOptions} [options] */ constructor( - target: ConstructorOrAbstractConstructor | MustUseConcerns, + target: ConstructorOrAbstractConstructor | UsesConcerns, concern: ConcernConstructor, alias: PropertyKey, key: PropertyKey, diff --git a/packages/support/src/concerns/use.ts b/packages/support/src/concerns/use.ts index 98cb8866..363f2ddb 100644 --- a/packages/support/src/concerns/use.ts +++ b/packages/support/src/concerns/use.ts @@ -26,7 +26,7 @@ import ConcernsInjector from "./ConcernsInjector"; * * @param {...Constructor | Configuration} concerns * - * @returns {(target: object) => MustUseConcerns} + * @returns {(target: object) => UsesConcerns} * * @throws {InjectionException} */ From 87bd991384ef08774e22539d9f96e8d2c2df4220 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 25 Feb 2024 14:06:52 +0100 Subject: [PATCH 334/424] Add Alias Conflict Error / Exception --- .../src/support/concerns/Injector.ts | 3 +- .../exceptions/AliasConflictException.ts | 39 +++++++ .../src/support/concerns/exceptions/index.ts | 2 + .../concerns/exceptions/AliasConflictError.ts | 106 ++++++++++++++++++ .../support/src/concerns/exceptions/index.ts | 2 + 5 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts create mode 100644 packages/support/src/concerns/exceptions/AliasConflictError.ts diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index 15324ab6..ab40fc7e 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -113,8 +113,7 @@ export default interface Injector * exists in target class' prototype chain, with the same name as the alias. * * @throws {UnsafeAliasException} If an alias points to an "unsafe" property or method in concern - * @throws {InjectionException} If unable to define "alias" in target class, e.g. due to failure when obtaining - * or defining [property descriptors]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#description}. + * @throws {InjectionException} If unable to define "alias" in target class. */ defineAlias( target: UsesConcerns, diff --git a/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts b/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts new file mode 100644 index 00000000..1a07828d --- /dev/null +++ b/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts @@ -0,0 +1,39 @@ +import { InjectionException } from "@aedart/contracts/support/concerns"; +import ConcernConstructor from "../ConcernConstructor"; +import {ConstructorOrAbstractConstructor} from "@aedart/contracts"; +import UsesConcerns from "../UsesConcerns"; + +/** + * Alias Conflict Exception + * + * To be thrown if an alias conflicts with another alias. + */ +export default interface AliasConflictException extends InjectionException +{ + /** + * The requested alias + * + * @readonly + * + * @type {PropertyKey} + */ + readonly alias: PropertyKey; + + /** + * The alias that {@link alias} conflicts with + * + * @readonly + * + * @type {PropertyKey} + */ + readonly conflictAlias: PropertyKey; + + /** + * The source class that defines the {@link conflictAlias} + * + * @readonly + * + * @type {ConstructorOrAbstractConstructor | UsesConcerns} + */ + readonly source: ConstructorOrAbstractConstructor | UsesConcerns; +} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/exceptions/index.ts b/packages/contracts/src/support/concerns/exceptions/index.ts index 598c08be..74fff8dd 100644 --- a/packages/contracts/src/support/concerns/exceptions/index.ts +++ b/packages/contracts/src/support/concerns/exceptions/index.ts @@ -1,3 +1,4 @@ +import AliasConflictException from "./AliasConflictException"; import AlreadyRegisteredException from "./AlreadyRegisteredException"; import BootException from "./BootException"; import ConcernException from "./ConcernException"; @@ -5,6 +6,7 @@ import InjectionException from "./InjectionException"; import NotRegisteredException from "./NotRegisteredException"; import UnsafeAliasException from "./UnsafeAliasException"; export { + type AliasConflictException, type AlreadyRegisteredException, type BootException, type ConcernException, diff --git a/packages/support/src/concerns/exceptions/AliasConflictError.ts b/packages/support/src/concerns/exceptions/AliasConflictError.ts new file mode 100644 index 00000000..320d08ad --- /dev/null +++ b/packages/support/src/concerns/exceptions/AliasConflictError.ts @@ -0,0 +1,106 @@ +import type {AliasConflictException, ConcernConstructor, UsesConcerns} from "@aedart/contracts/support/concerns"; +import InjectionError from "./InjectionError"; +import type {ConstructorOrAbstractConstructor} from "@aedart/contracts"; +import {getNameOrDesc} from "@aedart/support/reflections"; +import {configureCustomError} from "@aedart/support/exceptions"; + +export default class AliasConflictError extends InjectionError implements AliasConflictException +{ + /** + * The requested alias + * + * @readonly + * + * @type {PropertyKey} + */ + readonly #alias: PropertyKey; + + /** + * The alias that {@link alias} conflicts with + * + * @readonly + * + * @type {PropertyKey} + */ + readonly #conflictAlias: PropertyKey; + + /** + * The source class that defines the {@link conflictAlias} + * + * @readonly + * + * @type {ConstructorOrAbstractConstructor | UsesConcerns} + */ + readonly #source: ConstructorOrAbstractConstructor | UsesConcerns; + + /** + * Create a new Alias Conflict Error instance + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns} target + * @param {ConcernConstructor} concern + * @param {PropertyKey} alias + * @param {PropertyKey} conflictsWithAlias + * @param {ConstructorOrAbstractConstructor | UsesConcerns} source + * @param {ErrorOptions} [options] + */ + constructor( + target: ConstructorOrAbstractConstructor | UsesConcerns, + concern: ConcernConstructor, + alias: PropertyKey, + conflictsWithAlias: PropertyKey, + source: ConstructorOrAbstractConstructor | UsesConcerns, + options?: ErrorOptions + ) { + const reason: string = (target === source) + ? `Alias ${alias.toString()} conflicts with ${conflictsWithAlias.toString()}, in target ${getNameOrDesc(target)}` + : `Alias ${alias.toString()} conflicts with ${conflictsWithAlias.toString()} (alias defined in source ${getNameOrDesc(source)}), in target ${getNameOrDesc(target)}`; + super(target, concern, reason, options); + + configureCustomError(this); + + this.#alias = alias; + this.#conflictAlias = conflictsWithAlias; + this.#source = source; + + // Force set the properties in the cause + (this.cause as Record).alias = alias; + (this.cause as Record).conflictsWithAlias = conflictsWithAlias; + (this.cause as Record).source = source; + } + + /** + * The requested alias + * + * @readonly + * + * @type {PropertyKey} + */ + get alias(): PropertyKey + { + return this.#alias; + } + + /** + * The alias that {@link alias} conflicts with + * + * @readonly + * + * @type {PropertyKey} + */ + get conflictAlias(): PropertyKey + { + return this.#conflictAlias; + } + + /** + * The source class that defines the {@link conflictAlias} + * + * @readonly + * + * @type {ConstructorOrAbstractConstructor | UsesConcerns} + */ + get source(): ConstructorOrAbstractConstructor | UsesConcerns + { + return this.#source; + } +} \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/index.ts b/packages/support/src/concerns/exceptions/index.ts index 57be44a4..b91e1d80 100644 --- a/packages/support/src/concerns/exceptions/index.ts +++ b/packages/support/src/concerns/exceptions/index.ts @@ -1,3 +1,4 @@ +import AliasConflictError from "./AliasConflictError"; import AlreadyRegisteredError from "./AlreadyRegisteredError"; import BootError from "./BootError"; import ConcernError from "./ConcernError"; @@ -5,6 +6,7 @@ import InjectionError from "./InjectionError"; import NotRegisteredError from "./NotRegisteredError"; import UnsafeAliasError from "./UnsafeAliasError"; export { + AliasConflictError, AlreadyRegisteredError, BootError, ConcernError, From d12b2fafa7251dc28fb2f24f8126250dc3773bc1 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 25 Feb 2024 14:09:44 +0100 Subject: [PATCH 335/424] Change defineAliases(), allow throwing AliasConflictException This should offer more details for when working with an Injector. --- packages/contracts/src/support/concerns/Injector.ts | 3 ++- packages/support/src/concerns/ConcernsInjector.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index ab40fc7e..bf716d8d 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -90,7 +90,8 @@ export default interface Injector * * @returns {UsesConcerns} The modified target class * - * @throws {InjectionException} If case of alias naming conflicts. Or, if unable to define aliases in target class. + * @throws {AliasConflictException} If case of alias naming conflicts. + * @throws {InjectionException} If unable to define aliases in target class. */ defineAliases(target: UsesConcerns, configurations: Configuration[]): UsesConcerns; diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index d3099c89..10b15e69 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -195,7 +195,8 @@ export default class ConcernsInjector implements Injector // * // * @returns {UsesConcerns} The modified target class // * - // * @throws {InjectionException} If case of alias naming conflicts. Or, if unable to define aliases in target class. + // * @throws {AliasConflictException} If case of alias naming conflicts. + // * @throws {InjectionException} If unable to define aliases in target class. // */ // public defineAliases(target: UsesConcerns, configurations: Configuration[]): UsesConcerns // { From 15b2daae4f9085ce08d609a86078d28668da086e Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 25 Feb 2024 14:15:52 +0100 Subject: [PATCH 336/424] Improve description of defineAlias() Also removed the "C extends Concern" generic - no longer needed. --- .../src/support/concerns/Injector.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index bf716d8d..70fa4171 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -1,4 +1,3 @@ -import Concern from "./Concern"; import ConcernConstructor from "./ConcernConstructor"; import Configuration from "./Configuration"; import UsesConcerns from "./UsesConcerns"; @@ -86,40 +85,39 @@ export default interface Injector * @template T = object * * @param {UsesConcerns} target The target in which "aliases" must be defined in - * @param {Configuration[]} configurations List of concern injection configurations + * @param {Configuration[]} configurations List of concern injection configurations * * @returns {UsesConcerns} The modified target class * * @throws {AliasConflictException} If case of alias naming conflicts. * @throws {InjectionException} If unable to define aliases in target class. */ - defineAliases(target: UsesConcerns, configurations: Configuration[]): UsesConcerns; + defineAliases(target: UsesConcerns, configurations: Configuration[]): UsesConcerns; /** - * Defines an "alias" (proxy property or method) in target class' prototype, to a property or method - * in given concern. + * Defines an "alias" (proxy property or method) in target class' prototype, which points to a property or method + * in the given concern. * * **Note**: _Method will do nothing, if a property or method already exists in the target class' prototype * chain, with the same name as given "alias"._ - * - * @template C extends Concern + * * @template T = object * * @param {UsesConcerns} target The target in which "alias" must be defined in * @param {PropertyKey} alias Name of the "alias" in the target class (name of the proxy property or method) - * @param {PropertyKey} key Name of the property or method that the "alias" is for, in the concern class (`source`) - * @param {Constructor} source The concern class that holds the property or methods (`key`) + * @param {PropertyKey} key Name of the property or method that the "alias" points to, in the concern class (`source`) + * @param {Constructor} source The source concern class that contains the property or methods that is pointed to (`key`) * - * @returns {boolean} `true` if "alias" was in target class. `false` if not, e.g. a property or method already - * exists in target class' prototype chain, with the same name as the alias. + * @returns {boolean} `true` if "alias" was in target class. `false` if a property or method already exists in the + * target, with the same name as the "alias". * - * @throws {UnsafeAliasException} If an alias points to an "unsafe" property or method in concern + * @throws {UnsafeAliasException} If an alias points to an "unsafe" property or method in the source concern class. * @throws {InjectionException} If unable to define "alias" in target class. */ - defineAlias( + defineAlias( target: UsesConcerns, alias: PropertyKey, key: PropertyKey, - source: ConcernConstructor + source: ConcernConstructor ): boolean; } \ No newline at end of file From 2057f25d39889d8f3d024de34e44c6360e64951f Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 25 Feb 2024 14:46:32 +0100 Subject: [PATCH 337/424] Cleanup --- packages/support/src/reflections/getClassPropertyDescriptors.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/support/src/reflections/getClassPropertyDescriptors.ts b/packages/support/src/reflections/getClassPropertyDescriptors.ts index a56abbfd..7a19f3d5 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptors.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptors.ts @@ -45,7 +45,6 @@ export function getClassPropertyDescriptors(target: ConstructorOrAbstractConstru } // Merge evt. existing descriptor object with the one obtained from target. - if (Reflect.has(output, key)) { output[key] = merge() .using({ overwriteWithUndefined: false }) From 2f54b1752b7fd1c6e2c03aae48db1a1c5f34aa14 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 25 Feb 2024 17:48:22 +0100 Subject: [PATCH 338/424] Improve error message --- packages/support/src/concerns/exceptions/UnsafeAliasError.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/concerns/exceptions/UnsafeAliasError.ts b/packages/support/src/concerns/exceptions/UnsafeAliasError.ts index 02cfc8b4..5d30e3bd 100644 --- a/packages/support/src/concerns/exceptions/UnsafeAliasError.ts +++ b/packages/support/src/concerns/exceptions/UnsafeAliasError.ts @@ -48,7 +48,7 @@ export default class UnsafeAliasError extends InjectionError implements UnsafeAl message?: string, options?: ErrorOptions ) { - const reason: string = message || `Alias ${alias.toString()} points to "unsafe" property or method ${key.toString()} in concern ${getNameOrDesc(concern)}, in target ${getNameOrDesc(target)}`; + const reason: string = message || `Alias "${alias.toString()}" in target ${getNameOrDesc(target)} points to unsafe property or method: "${key.toString()}", in concern ${getNameOrDesc(concern)}.`; super(target, concern, reason, options); configureCustomError(this); From 007cbecd1d5cc8a867e8da90ac7ff43f26dfe213 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 25 Feb 2024 18:05:50 +0100 Subject: [PATCH 339/424] Make capture stack trace optional Well, it appears that browser are able to automatically set a stack trace correctly. So now this will be optional - just in case. --- .../support/src/exceptions/configureCustomError.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/support/src/exceptions/configureCustomError.ts b/packages/support/src/exceptions/configureCustomError.ts index 6947adc0..bf64a7c5 100644 --- a/packages/support/src/exceptions/configureCustomError.ts +++ b/packages/support/src/exceptions/configureCustomError.ts @@ -2,10 +2,10 @@ import { configureStackTrace } from "./configureStackTrace"; /** * Configures the custom error - * - * **Note**: _Method configures error by setting its [stack trace]{@link configureStackTrace} - * and setting the error's common properties, e.g. name_ * + * **Note**: _Method sets the custom error's name and optionally captures a [stack trace]{@link configureStackTrace} + * and sets it as the custom error's stack._ + * * **Example**: * ``` * class MyCustomError extends Error @@ -22,12 +22,15 @@ import { configureStackTrace } from "./configureStackTrace"; * @template T extends Error * * @param {Error} error + * @param {boolean} [captureStackTrace=false] * * @return {Error} */ -export function configureCustomError(error: T): T +export function configureCustomError(error: T, captureStackTrace: boolean = false): T { - configureStackTrace(error); + if (captureStackTrace) { + configureStackTrace(error); + } error.name = Reflect.getPrototypeOf(error)?.constructor.name as string; From b898e9b1aa8e401a5e15d850a5f01e013abd602c Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 25 Feb 2024 19:55:50 +0100 Subject: [PATCH 340/424] Fix style --- .../src/support/concerns/exceptions/AliasConflictException.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts b/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts index 1a07828d..c789cbf3 100644 --- a/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts +++ b/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts @@ -1,5 +1,4 @@ import { InjectionException } from "@aedart/contracts/support/concerns"; -import ConcernConstructor from "../ConcernConstructor"; import {ConstructorOrAbstractConstructor} from "@aedart/contracts"; import UsesConcerns from "../UsesConcerns"; From 7b8a93f841c0a9cc09ba7e8f0fb83322c7a2f03a Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 25 Feb 2024 20:42:17 +0100 Subject: [PATCH 341/424] Add defineAlias() implementation --- .../support/src/concerns/ConcernsInjector.ts | 292 +++++++++++++++--- .../concerns/injector/defineAlias.test.js | 276 +++++++++++++++++ 2 files changed, 527 insertions(+), 41 deletions(-) create mode 100644 tests/browser/packages/support/concerns/injector/defineAlias.test.js diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 10b15e69..9609be9c 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -14,14 +14,15 @@ import { } from "@aedart/contracts/support/concerns"; import { getAllParentsOfClass, - getNameOrDesc + getNameOrDesc, + getClassPropertyDescriptors, } from "@aedart/support/reflections"; -import { - AlreadyRegisteredError, - InjectionError -} from "./exceptions"; +import AlreadyRegisteredError from './exceptions/AlreadyRegisteredError'; +import InjectionError from './exceptions/InjectionError'; +import UnsafeAliasError from './exceptions/UnsafeAliasError'; import ConcernsContainer from './ConcernsContainer'; import ConfigurationFactory from "./ConfigurationFactory"; +import { isUnsafeKey } from "./isUnsafeKey"; /** * A map of the concern owner instances and their concerns container @@ -59,6 +60,18 @@ export default class ConcernsInjector implements Injector * @protected */ protected factory: Factory; + + /** + * In-memory cache property descriptors for target class and concern classes + * + * @type {WeakMap>} + * + * @private + */ + #cachedDescriptors: WeakMap< + ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, + Record + > = new WeakMap(); /** * Create a new Concerns Injector instance @@ -181,7 +194,7 @@ export default class ConcernsInjector implements Injector return target; } - + // /** // * Defines "aliases" (proxy properties and methods) in target class' prototype, such that they // * point to the properties and methods available in the concern classes. @@ -191,52 +204,73 @@ export default class ConcernsInjector implements Injector // * @template T = object // * // * @param {UsesConcerns} target The target in which "aliases" must be defined in - // * @param {Configuration[]} configurations List of concern injection configurations + // * @param {Configuration[]} configurations List of concern injection configurations // * // * @returns {UsesConcerns} The modified target class // * // * @throws {AliasConflictException} If case of alias naming conflicts. // * @throws {InjectionException} If unable to define aliases in target class. // */ - // public defineAliases(target: UsesConcerns, configurations: Configuration[]): UsesConcerns + // defineAliases(target: UsesConcerns, configurations: Configuration[]): UsesConcerns // { // // TODO: implement this method... + // // TODO: cache target property descriptors + // // TODO: cache concern property descriptors + // // TODO: - delete concern property descriptors after its aliases are defined + // // TODO: clear all cached descriptors, after all aliases defined // // return target; // } - // - // /** - // * Defines an "alias" (proxy property or method) in target class' prototype, to a property or method - // * in given concern. - // * - // * **Note**: _Method will do nothing, if a property or method already exists in the target class' prototype - // * chain, with the same name as given "alias"._ - // * - // * @template T = object - // * - // * @param {UsesConcerns} target The target in which "alias" must be defined in - // * @param {PropertyKey} alias Name of the "alias" in the target class (name of the proxy property or method) - // * @param {PropertyKey} key Name of the property or method that the "alias" is for, in the concern class (`source`) - // * @param {Constructor} source The concern class that holds the property or methods (`key`) - // * - // * @returns {boolean} `true` if "alias" was in target class. `false` if not, e.g. a property or method already - // * exists in target class' prototype chain, with the same name as the alias. - // * - // * @throws {UnsafeAliasException} If an alias points to an "unsafe" property or method in concern - // * @throws {InjectionException} If unable to define "alias" in target class, e.g. due to failure when obtaining - // * or defining [property descriptors]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#description}. - // */ - // public defineAlias( - // target: UsesConcerns, - // alias: PropertyKey, - // key: PropertyKey, - // source: Constructor - // ): boolean - // { - // // TODO: implement this method... - // - // return false; - // } + + /** + * Defines an "alias" (proxy property or method) in target class' prototype, which points to a property or method + * in the given concern. + * + * **Note**: _Method will do nothing, if a property or method already exists in the target class' prototype + * chain, with the same name as given "alias"._ + * + * @template T = object + * + * @param {UsesConcerns} target The target in which "alias" must be defined in + * @param {PropertyKey} alias Name of the "alias" in the target class (name of the proxy property or method) + * @param {PropertyKey} key Name of the property or method that the "alias" points to, in the concern class (`source`) + * @param {Constructor} source The source concern class that contains the property or methods that is pointed to (`key`) + * + * @returns {boolean} `true` if "alias" was in target class. `false` if a property or method already exists in the + * target, with the same name as the "alias". + * + * @throws {UnsafeAliasError} If an alias points to an "unsafe" property or method in the source concern class. + * @throws {InjectionException} If unable to define "alias" in target class. + */ + public defineAlias( + target: UsesConcerns, + alias: PropertyKey, + key: PropertyKey, + source: ConcernConstructor + ): boolean + { + // Abort if key is "unsafe" + if (this.isUnsafe(key)) { + throw new UnsafeAliasError(target, source, alias, key); + } + + // Skip if a property key already exists with same name as the "alias" + const targetDescriptors = this.getDescriptorsFor(target); + if (Reflect.has(targetDescriptors, alias)) { + return false; + } + + // Abort if unable to find descriptor that matches given key in concern class. + const concernDescriptors = this.getDescriptorsFor(source); + if (!Reflect.has(concernDescriptors, key)) { + throw new InjectionError(target, source, `"${key.toString()}" does not exist in concern ${getNameOrDesc(source)} - attempted aliased as "${alias.toString()}" in target ${getNameOrDesc(target)}`); + } + + // Define the proxy property or method, using the concern's property descriptor to determine what must be defined. + const proxy = this.resolveProxyDescriptor(key, source, concernDescriptors[key]) + + return this.definePropertyInTarget(target.prototype, alias, proxy) !== undefined; + } /** * Normalises given concerns into a list of concern configurations @@ -374,6 +408,168 @@ export default class ConcernsInjector implements Injector return null; } + /** + * Resolves the proxy property descriptor for given key in source concern + * + * @param {PropertyKey} key + * @param {ConcernConstructor} source + * @param {PropertyDescriptor} keyDescriptor Descriptor of `key` in `source` + * + * @returns {PropertyDescriptor} Descriptor to be used for defining alias in a target class + * + * @protected + */ + protected resolveProxyDescriptor(key: PropertyKey, source: ConcernConstructor, keyDescriptor: PropertyDescriptor): PropertyDescriptor + { + const proxy: PropertyDescriptor = Object.assign(Object.create(null), { + configurable: keyDescriptor.configurable, + enumerable: keyDescriptor.enumerable, + // writable: keyDescriptor.writable // Do not specify here... + }); + + // A descriptor can only have an accessor, a value or writable attribute. Depending on the "value" + // a different kind of proxy must be defined. + const hasValue: boolean = Reflect.has(keyDescriptor, 'value'); + + if (hasValue && typeof keyDescriptor.value == 'function') { + proxy.value = this.makeMethodProxy(key, source); + } else if (hasValue) { + // When value is not a function, it could be a writable attribute. + // To alias such a property, we first define a getter for it. + proxy.get = this.makeGetPropertyProxy(key, source); + + // Secondly, if the property is writable, then define a setter for + if (keyDescriptor.writable) { + proxy.set = this.makeSetPropertyProxy(key, source); + } + } else { + // Otherwise, the property can a getter and or a setter... + if (Reflect.has(keyDescriptor, 'get')) { + proxy.get = this.makeGetPropertyProxy(key, source); + } + + if (Reflect.has(keyDescriptor, 'set')) { + proxy.set = this.makeSetPropertyProxy(key, source); + } + } + + return proxy; + } + + /** + * Returns a new proxy "method" for given method in this concern + * + * @param {PropertyKey} method + * @param {ConcernConstructor} concern + * + * @returns {(...args: any[]) => any} + * + * @protected + */ + protected makeMethodProxy(method: PropertyKey, concern: ConcernConstructor) + { + return function( + ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + // @ts-expect-error This = concern instance + return (this as Owner)[CONCERNS].call(concern, method, ...args); + } + } + + /** + * Returns a new proxy "get" for given property in this concern + * + * @param {PropertyKey} property + * @param {ConcernConstructor} concern + * + * @returns {() => any} + * + * @protected + */ + protected makeGetPropertyProxy(property: PropertyKey, concern: ConcernConstructor) + { + return function(): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + // @ts-expect-error This = concern instance + return (this as Owner)[CONCERNS].getProperty(concern, property); + } + } + + /** + * Returns a new proxy "set" for given property in this concern + * + * @param {PropertyKey} property + * @param {ConcernConstructor} concern + * + * @returns {(value: any) => void} + * + * @protected + */ + protected makeSetPropertyProxy(property: PropertyKey, concern: ConcernConstructor) + { + return function( + value: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): void + { + // @ts-expect-error This = concern instance + (this as Owner)[CONCERNS].setProperty(concern, property, value); + } + } + + /** + * Returns property descriptors for given target class (recursively) + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {boolean} [force=false] If `true` then method will not return evt. cached descriptors. + * @param {boolean} [cache=false] Caches the descriptors if `true`. + * + * @returns {Record} + * + * @protected + */ + protected getDescriptorsFor( + target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, + force: boolean = false, + cache: boolean = false + ): Record + { + if (!force && this.#cachedDescriptors.has(target)) { + return this.#cachedDescriptors.get(target) as Record; + } + + const descriptors = getClassPropertyDescriptors(target, true); + if (cache) { + this.#cachedDescriptors.set(target, descriptors); + } + + return descriptors; + } + + /** + * Deletes cached property descriptors for target + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target + * + * @returns {boolean} `true` if cached descriptors were removed, `false` if none were cached + * + * @protected + */ + protected deleteCachedDescriptorsFor(target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor): boolean + { + return this.#cachedDescriptors.delete(target); + } + + /** + * Clears all cached property descriptors + * + * @protected + */ + protected clearCachedDescriptors(): void + { + this.#cachedDescriptors = new WeakMap(); + } + /** * Returns a new concern configuration factory instance * @@ -385,4 +581,18 @@ export default class ConcernsInjector implements Injector { return new ConfigurationFactory(); } + + /** + * Determine if key is "unsafe" + * + * @param {PropertyKey} key + * + * @returns {boolean} + * + * @protected + */ + protected isUnsafe(key: PropertyKey): boolean + { + return isUnsafeKey(key); + } } \ No newline at end of file diff --git a/tests/browser/packages/support/concerns/injector/defineAlias.test.js b/tests/browser/packages/support/concerns/injector/defineAlias.test.js new file mode 100644 index 00000000..fd663485 --- /dev/null +++ b/tests/browser/packages/support/concerns/injector/defineAlias.test.js @@ -0,0 +1,276 @@ +import makeConcernsInjector from "../helpers/makeConcernsInjector"; +import { AbstractConcern, InjectionError, UnsafeAliasError } from "@aedart/support/concerns"; + +describe('@aedart/support/concerns', () => { + describe('ConcernsInjector', () => { + describe('defineAlias()', () => { + + it('fails if key is "unsafe"', () => { + class ConcernA extends AbstractConcern {} + + class A {} + + const injector = makeConcernsInjector(A); + + // --------------------------------------------------------------------------- // + + const callback = () => { + injector.defineAlias(A, 'foo', 'constructor', ConcernA); + } + + expect(callback) + .toThrowError(UnsafeAliasError); + }); + + it('does not define alias if property or method already exists in target"', () => { + class ConcernA extends AbstractConcern {} + + class A { + get foo() {} + + bar() {} + } + + const injector = makeConcernsInjector(A); + + // --------------------------------------------------------------------------- // + + const resultA = injector.defineAlias(A, 'foo', 'foo', ConcernA); + const resultB = injector.defineAlias(A, 'bar', 'bar', ConcernA); + + expect(resultA) + .withContext('Property alias was defined when it SHOULD NOT be') + .toBeFalse(); + + expect(resultB) + .withContext('Method alias was defined when it SHOULD NOT be') + .toBeFalse(); + }); + + it('fails if key does not exist in concern', () => { + class ConcernA extends AbstractConcern {} + + class A {} + + const injector = makeConcernsInjector(A); + + // --------------------------------------------------------------------------- // + + const callback = () => { + injector.defineAlias(A, 'foo', 'bar', ConcernA); + } + + expect(callback) + .toThrowError(InjectionError); + }); + + it('can define proxies in target prototype', () => { + class ConcernA extends AbstractConcern { + foo() {} + + get message() {} + set message(value) {} + + // Writable attribute + //title = 'ABC' // NOTE: This is defined below. Transpilers move this into the constructor (...uh) + } + Reflect.defineProperty(ConcernA.prototype, 'title', { + value: 'ABC', + writable: true, + enumerable: true, + configurable: true + }); + + class A {} + + const injector = makeConcernsInjector(A); + + // --------------------------------------------------------------------------- // + + const resultA = injector.defineAlias(A, 'foo', 'foo', ConcernA); + const resultB = injector.defineAlias(A, 'message', 'message', ConcernA); + const resultC = injector.defineAlias(A, 'title', 'title', ConcernA); + + // Debug + // console.log('A.prototype', Reflect.ownKeys(A.prototype)); + + expect(resultA) + .withContext('Method "foo" alias failed?!') + .toBeTrue(); + expect(Reflect.has(A.prototype, 'foo')) + .withContext('Method "foo" was not defined in target prototype') + .toBeTrue(); + + expect(resultB) + .withContext('property "message" alias failed?!') + .toBeTrue(); + expect(Reflect.has(A.prototype, 'foo')) + .withContext('property "message" was not defined in target prototype') + .toBeTrue(); + + expect(resultC) + .withContext('writable property "title" alias failed?!') + .toBeTrue(); + expect(Reflect.has(A.prototype, 'foo')) + .withContext('writable property "title" was not defined in target prototype') + .toBeTrue(); + }); + + it('can interact with proxy properties and methods in target instance', () => { + + /** + * @ mixin Uhm, only works on objects! + */ + class ConcernA extends AbstractConcern { + + /** + * Say hi to ... + * + * @param {string} name + * + * @returns {string} + * + * @memberof ConcernA + */ + greetings(name) { + return `Hi ${name}`; + } + + #msg = null; + get message() { + return this.#msg; + } + set message(value) { + this.#msg = value + } + + // Writable attribute + //title = 'ABC' // NOTE: This is defined below. Transpilers move this into the constructor (...uh) + } + Reflect.defineProperty(ConcernA.prototype, 'title', { + value: 'ABC', + writable: true, + enumerable: true, + configurable: true + }); + + /** + * A (with a bit of JSDoc experiment) + * + * @ mixes ConcernA Uhm, only works on objects! + * @ borrows ConcernA#greetings as greetings Does not seem to work + * @ borrows ConcernA#greetings as A#greetings Does not seem to work + * @ property {ConcernA.greetings} greetings Does not seem to work + * @ property {typeof ConcernA.greetings} greetings Does not seem to work + * @property {(name: string) => string} greetings Say hi + * @property {string|null} message Set a message + * @property {string} title A title + */ + class A { + } + + const injector = makeConcernsInjector(A); + const target = injector.defineContainer( + injector.defineConcerns(A, [ + ConcernA, + ]) + ); + + // --------------------------------------------------------------------------- // + + injector.defineAlias(target, 'greetings', 'greetings', ConcernA); + injector.defineAlias(target, 'message', 'message', ConcernA); + injector.defineAlias(target, 'title', 'title', ConcernA); + + // Debug + // console.log('A.prototype', Reflect.ownKeys(A.prototype)); + + // --------------------------------------------------------------------------- // + + const instance = new A(); + + const result = instance.greetings('Timmy'); + expect(result) + .withContext('Method interaction failed') + .toBe('Hi Timmy'); + + const message = 'There goes an elephant...'; + instance.message = message; + expect(instance.message) + .withContext('getter/setter interaction failed') + .toBe(message); + + + expect(instance.title) + .withContext('getter/setter interaction for "writable" property failed (A)') + .toBe('ABC'); + + const newTitle = 'Zookeeper'; + instance.title = newTitle; + expect(instance.title) + .withContext('getter/setter interaction for "writable" property failed (B)') + .toBe(newTitle); + }); + + it('alias remains for new target instance', () => { + + const defaultCategory = 'N/A'; + + class ConcernA extends AbstractConcern { + + #category = defaultCategory; + get category() { + return this.#category; + } + set category(value) { + this.#category = value + } + } + + /** + * @property {string} category A category name + */ + class A {} + + class B extends A {} + + const injector = makeConcernsInjector(A); + const target = injector.defineContainer( + injector.defineConcerns(A, [ + ConcernA, + ]) + ); + + // --------------------------------------------------------------------------- // + + injector.defineAlias(target, 'category', 'category', ConcernA); + + // Debug + // console.log('A.prototype', Reflect.ownKeys(A.prototype)); + + const instanceA = new A(); + expect(instanceA.category) + .withContext('Incorrect default title for A') + .toBe(defaultCategory); + + const newCategoryA = 'Fish'; + instanceA.category = newCategoryA; + expect(instanceA.category) + .withContext('Incorrect changed title for A') + .toBe(newCategoryA); + + const instanceB = new B(); + expect(instanceB.category) + .withContext('Incorrect default title for B') + .toBe(defaultCategory); + + const newCategoryB = 'Dogs'; + instanceB.category = newCategoryB; + expect(instanceB.category) + .withContext('Incorrect changed title for B') + .toBe(newCategoryB); + + }); + }); + }); +}); \ No newline at end of file From 7b0b0391b0b8c93eea9edc657c5d6d2dc2d1307b Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 07:19:22 +0100 Subject: [PATCH 342/424] Improve internal comments --- packages/support/src/concerns/ConcernsInjector.ts | 6 +++--- .../packages/support/concerns/injector/defineAlias.test.js | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 9609be9c..04415b28 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -434,11 +434,11 @@ export default class ConcernsInjector implements Injector if (hasValue && typeof keyDescriptor.value == 'function') { proxy.value = this.makeMethodProxy(key, source); } else if (hasValue) { - // When value is not a function, it could be a writable attribute. - // To alias such a property, we first define a getter for it. + // When value is not a function, it could be a readonly property... proxy.get = this.makeGetPropertyProxy(key, source); - // Secondly, if the property is writable, then define a setter for + // However, if the descriptor claims that its writable, then + // a setter must be defined too. if (keyDescriptor.writable) { proxy.set = this.makeSetPropertyProxy(key, source); } diff --git a/tests/browser/packages/support/concerns/injector/defineAlias.test.js b/tests/browser/packages/support/concerns/injector/defineAlias.test.js index fd663485..b6c4e53b 100644 --- a/tests/browser/packages/support/concerns/injector/defineAlias.test.js +++ b/tests/browser/packages/support/concerns/injector/defineAlias.test.js @@ -189,11 +189,13 @@ describe('@aedart/support/concerns', () => { const instance = new A(); + // Method const result = instance.greetings('Timmy'); expect(result) .withContext('Method interaction failed') .toBe('Hi Timmy'); + // Getter / Setter (Accessor) const message = 'There goes an elephant...'; instance.message = message; expect(instance.message) @@ -201,6 +203,7 @@ describe('@aedart/support/concerns', () => { .toBe(message); + // "Writable" property, defined on prototype expect(instance.title) .withContext('getter/setter interaction for "writable" property failed (A)') .toBe('ABC'); From 3b9af9a25e4d9e69cf325a207d9c1650e02a9177 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 08:34:05 +0100 Subject: [PATCH 343/424] Improve error message --- .../support/src/concerns/exceptions/AliasConflictError.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/support/src/concerns/exceptions/AliasConflictError.ts b/packages/support/src/concerns/exceptions/AliasConflictError.ts index 320d08ad..2a59c678 100644 --- a/packages/support/src/concerns/exceptions/AliasConflictError.ts +++ b/packages/support/src/concerns/exceptions/AliasConflictError.ts @@ -52,8 +52,8 @@ export default class AliasConflictError extends InjectionError implements AliasC options?: ErrorOptions ) { const reason: string = (target === source) - ? `Alias ${alias.toString()} conflicts with ${conflictsWithAlias.toString()}, in target ${getNameOrDesc(target)}` - : `Alias ${alias.toString()} conflicts with ${conflictsWithAlias.toString()} (alias defined in source ${getNameOrDesc(source)}), in target ${getNameOrDesc(target)}`; + ? `Alias "${alias.toString()}" conflicts with alias "${conflictsWithAlias.toString()}", in target ${getNameOrDesc(target)}` + : `Alias "${alias.toString()}" conflicts with alias "${conflictsWithAlias.toString()}" (defined in source ${getNameOrDesc(source)}), in target ${getNameOrDesc(target)}`; super(target, concern, reason, options); configureCustomError(this); From 42a662675a16a1c54ff8c5cc67be6d9d7d7700cc Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 08:38:30 +0100 Subject: [PATCH 344/424] Simplify exception, remove the "conflict Alias" The alias and "conflict alias" are the same... duh! --- .../exceptions/AliasConflictException.ts | 14 ++--- .../concerns/exceptions/AliasConflictError.ts | 52 ++++++------------- 2 files changed, 20 insertions(+), 46 deletions(-) diff --git a/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts b/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts index c789cbf3..569085cb 100644 --- a/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts +++ b/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts @@ -10,7 +10,8 @@ import UsesConcerns from "../UsesConcerns"; export default interface AliasConflictException extends InjectionException { /** - * The requested alias + * The requested alias that conflicts with another alias + * of the same name. * * @readonly * @@ -19,16 +20,7 @@ export default interface AliasConflictException extends InjectionException readonly alias: PropertyKey; /** - * The alias that {@link alias} conflicts with - * - * @readonly - * - * @type {PropertyKey} - */ - readonly conflictAlias: PropertyKey; - - /** - * The source class that defines the {@link conflictAlias} + * The source class that defines that originally defined the alias * * @readonly * diff --git a/packages/support/src/concerns/exceptions/AliasConflictError.ts b/packages/support/src/concerns/exceptions/AliasConflictError.ts index 2a59c678..e2385038 100644 --- a/packages/support/src/concerns/exceptions/AliasConflictError.ts +++ b/packages/support/src/concerns/exceptions/AliasConflictError.ts @@ -1,13 +1,19 @@ -import type {AliasConflictException, ConcernConstructor, UsesConcerns} from "@aedart/contracts/support/concerns"; +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { AliasConflictException, ConcernConstructor, UsesConcerns } from "@aedart/contracts/support/concerns"; import InjectionError from "./InjectionError"; -import type {ConstructorOrAbstractConstructor} from "@aedart/contracts"; -import {getNameOrDesc} from "@aedart/support/reflections"; -import {configureCustomError} from "@aedart/support/exceptions"; +import { getNameOrDesc } from "@aedart/support/reflections"; +import { configureCustomError } from "@aedart/support/exceptions"; +/** + * Alias Conflict Error + * + * @see AliasConflictException + */ export default class AliasConflictError extends InjectionError implements AliasConflictException { /** - * The requested alias + * The requested alias that conflicts with another alias + * of the same name. * * @readonly * @@ -16,16 +22,7 @@ export default class AliasConflictError extends InjectionError implements AliasC readonly #alias: PropertyKey; /** - * The alias that {@link alias} conflicts with - * - * @readonly - * - * @type {PropertyKey} - */ - readonly #conflictAlias: PropertyKey; - - /** - * The source class that defines the {@link conflictAlias} + * The source class that defines that originally defined the alias * * @readonly * @@ -39,7 +36,6 @@ export default class AliasConflictError extends InjectionError implements AliasC * @param {ConstructorOrAbstractConstructor | UsesConcerns} target * @param {ConcernConstructor} concern * @param {PropertyKey} alias - * @param {PropertyKey} conflictsWithAlias * @param {ConstructorOrAbstractConstructor | UsesConcerns} source * @param {ErrorOptions} [options] */ @@ -47,29 +43,27 @@ export default class AliasConflictError extends InjectionError implements AliasC target: ConstructorOrAbstractConstructor | UsesConcerns, concern: ConcernConstructor, alias: PropertyKey, - conflictsWithAlias: PropertyKey, source: ConstructorOrAbstractConstructor | UsesConcerns, options?: ErrorOptions ) { const reason: string = (target === source) - ? `Alias "${alias.toString()}" conflicts with alias "${conflictsWithAlias.toString()}", in target ${getNameOrDesc(target)}` - : `Alias "${alias.toString()}" conflicts with alias "${conflictsWithAlias.toString()}" (defined in source ${getNameOrDesc(source)}), in target ${getNameOrDesc(target)}`; + ? `Alias "${alias.toString()}" conflicts with alias "${alias.toString()}", in target ${getNameOrDesc(target)}` + : `Alias "${alias.toString()}" conflicts with alias "${alias.toString()}" (defined in source ${getNameOrDesc(source)}), in target ${getNameOrDesc(target)}`; super(target, concern, reason, options); configureCustomError(this); this.#alias = alias; - this.#conflictAlias = conflictsWithAlias; this.#source = source; // Force set the properties in the cause (this.cause as Record).alias = alias; - (this.cause as Record).conflictsWithAlias = conflictsWithAlias; (this.cause as Record).source = source; } /** - * The requested alias + * The requested alias that conflicts with another alias + * of the same name. * * @readonly * @@ -81,19 +75,7 @@ export default class AliasConflictError extends InjectionError implements AliasC } /** - * The alias that {@link alias} conflicts with - * - * @readonly - * - * @type {PropertyKey} - */ - get conflictAlias(): PropertyKey - { - return this.#conflictAlias; - } - - /** - * The source class that defines the {@link conflictAlias} + * The source class that defines that originally defined the alias * * @readonly * From 9620a82f9644b89e95757cadf10e5bedfcc68c98 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 09:02:59 +0100 Subject: [PATCH 345/424] Improve Alias Conflict Exception, add property key This makes it a bit easier to understand what alias conflicts, and what property it attempts to point to. --- .../exceptions/AliasConflictException.ts | 20 ++++++-- .../concerns/exceptions/AliasConflictError.ts | 49 ++++++++++++++----- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts b/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts index 569085cb..e9214934 100644 --- a/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts +++ b/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts @@ -1,6 +1,7 @@ -import { InjectionException } from "@aedart/contracts/support/concerns"; -import {ConstructorOrAbstractConstructor} from "@aedart/contracts"; +import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import InjectionException from "./InjectionException"; import UsesConcerns from "../UsesConcerns"; +import { Alias } from '../types' /** * Alias Conflict Exception @@ -15,12 +16,21 @@ export default interface AliasConflictException extends InjectionException * * @readonly * - * @type {PropertyKey} + * @type {Alias} */ - readonly alias: PropertyKey; + readonly alias: Alias; /** - * The source class that defines that originally defined the alias + * the property key that the conflicting alias points to + * + * @readonly + * + * @type {Alias} + */ + readonly key: PropertyKey; + + /** + * The source class (e.g. parent class) that defines that originally defined the alias * * @readonly * diff --git a/packages/support/src/concerns/exceptions/AliasConflictError.ts b/packages/support/src/concerns/exceptions/AliasConflictError.ts index e2385038..ab6deb59 100644 --- a/packages/support/src/concerns/exceptions/AliasConflictError.ts +++ b/packages/support/src/concerns/exceptions/AliasConflictError.ts @@ -1,5 +1,5 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; -import type { AliasConflictException, ConcernConstructor, UsesConcerns } from "@aedart/contracts/support/concerns"; +import type { AliasConflictException, ConcernConstructor, UsesConcerns, Alias } from "@aedart/contracts/support/concerns"; import InjectionError from "./InjectionError"; import { getNameOrDesc } from "@aedart/support/reflections"; import { configureCustomError } from "@aedart/support/exceptions"; @@ -16,15 +16,27 @@ export default class AliasConflictError extends InjectionError implements AliasC * of the same name. * * @readonly + * @private * - * @type {PropertyKey} + * @type {Alias} */ - readonly #alias: PropertyKey; + readonly #alias: Alias; /** - * The source class that defines that originally defined the alias + * the property key that the conflicting alias points to * * @readonly + * @private + * + * @type {PropertyKey} + */ + readonly #key: PropertyKey; + + /** + * The source class (e.g. parent class) that defines that originally defined the alias + * + * @readonly + * @private * * @type {ConstructorOrAbstractConstructor | UsesConcerns} */ @@ -35,25 +47,28 @@ export default class AliasConflictError extends InjectionError implements AliasC * * @param {ConstructorOrAbstractConstructor | UsesConcerns} target * @param {ConcernConstructor} concern - * @param {PropertyKey} alias + * @param {Alias} alias + * @param {PropertyKey} key * @param {ConstructorOrAbstractConstructor | UsesConcerns} source * @param {ErrorOptions} [options] */ constructor( target: ConstructorOrAbstractConstructor | UsesConcerns, concern: ConcernConstructor, - alias: PropertyKey, + alias: Alias, + key: PropertyKey, source: ConstructorOrAbstractConstructor | UsesConcerns, options?: ErrorOptions ) { const reason: string = (target === source) - ? `Alias "${alias.toString()}" conflicts with alias "${alias.toString()}", in target ${getNameOrDesc(target)}` - : `Alias "${alias.toString()}" conflicts with alias "${alias.toString()}" (defined in source ${getNameOrDesc(source)}), in target ${getNameOrDesc(target)}`; + ? `Alias "${alias.toString()}" for property key "${key.toString()}" in concern ${getNameOrDesc(concern)} conflicts with alias "${alias.toString()}", in target ${getNameOrDesc(target)}` + : `Alias "${alias.toString()}" for property key "${key.toString()}" in concern ${getNameOrDesc(concern)} conflicts with alias "${alias.toString()}" (defined in parent ${getNameOrDesc(source)}), in target ${getNameOrDesc(target)}`; super(target, concern, reason, options); configureCustomError(this); this.#alias = alias; + this.#key = key; this.#source = source; // Force set the properties in the cause @@ -67,15 +82,27 @@ export default class AliasConflictError extends InjectionError implements AliasC * * @readonly * - * @type {PropertyKey} + * @type {Alias} */ - get alias(): PropertyKey + get alias(): Alias { return this.#alias; } /** - * The source class that defines that originally defined the alias + * the property key that the conflicting alias points to + * + * @readonly + * + * @type {PropertyKey} + */ + get key(): PropertyKey + { + return this.#key; + } + + /** + * The source class (e.g. parent class) that defines that originally defined the alias * * @readonly * From 378b05fcf907808771e1a1091f17c55ba658f913 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 10:28:17 +0100 Subject: [PATCH 346/424] Add ALIASES symbol This helps keep track of what aliases a given target class applies. Doing allows an Injector to prevent alias conflicts. --- .../src/support/concerns/UsesConcerns.ts | 19 +++++++++++++++++-- .../contracts/src/support/concerns/index.ts | 9 +++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/UsesConcerns.ts b/packages/contracts/src/support/concerns/UsesConcerns.ts index 97f375c8..61a043c5 100644 --- a/packages/contracts/src/support/concerns/UsesConcerns.ts +++ b/packages/contracts/src/support/concerns/UsesConcerns.ts @@ -1,5 +1,5 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; -import { CONCERN_CLASSES } from "./index"; +import { CONCERN_CLASSES, ALIASES, Alias } from "./index"; import ConcernConstructor from "./ConcernConstructor"; import Owner from "./Owner"; @@ -32,9 +32,24 @@ export default interface UsesConcerns * concern classes are included recursively in this list, in the order * that they have been registered._ * + * **Note**: _This property is usually automatically defined and populated + * by an {@link Injector}, and used to prevent duplicate concern injections._ + * * @static * - * @type {ConcernConstructor} + * @type {ConcernConstructor[]} */ [CONCERN_CLASSES]: ConcernConstructor[]; + + /** + * List of aliases applied in this target class. + * + * **Note**: _This property is usually automatically defined and populated + * by an {@link Injector}, and used to prevent alias conflicts._ + * + * @static + * + * @type {Alias[]} + */ + [ALIASES]: Alias[]; } \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 092823cf..915ca3a6 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -37,6 +37,15 @@ export const PROVIDES : unique symbol = Symbol('concern_provides'); */ export const CONCERN_CLASSES: unique symbol = Symbol('concern_classes'); +/** + * Symbol used to list the aliases applied in a target class + * + * @see {UsesConcerns} + * + * @type {Symbol} + */ +export const ALIASES: unique symbol = Symbol('aliases'); + /** * Symbol used to define a "concerns container" property inside a target class' prototype * From abc576828c938fc98efd0be712cbc1597ddef602 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 11:35:46 +0100 Subject: [PATCH 347/424] Improve error message (again) --- .../support/src/concerns/exceptions/AliasConflictError.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/support/src/concerns/exceptions/AliasConflictError.ts b/packages/support/src/concerns/exceptions/AliasConflictError.ts index ab6deb59..0d94f757 100644 --- a/packages/support/src/concerns/exceptions/AliasConflictError.ts +++ b/packages/support/src/concerns/exceptions/AliasConflictError.ts @@ -61,8 +61,8 @@ export default class AliasConflictError extends InjectionError implements AliasC options?: ErrorOptions ) { const reason: string = (target === source) - ? `Alias "${alias.toString()}" for property key "${key.toString()}" in concern ${getNameOrDesc(concern)} conflicts with alias "${alias.toString()}", in target ${getNameOrDesc(target)}` - : `Alias "${alias.toString()}" for property key "${key.toString()}" in concern ${getNameOrDesc(concern)} conflicts with alias "${alias.toString()}" (defined in parent ${getNameOrDesc(source)}), in target ${getNameOrDesc(target)}`; + ? `Alias "${alias.toString()}" for property key "${key.toString()}" (concern ${getNameOrDesc(concern)}) conflicts with previous defined alias "${alias.toString()}", in target ${getNameOrDesc(target)}` + : `Alias "${alias.toString()}" for property key "${key.toString()}" (concern ${getNameOrDesc(concern)}) conflicts with previous defined alias "${alias.toString()}" (defined in parent ${getNameOrDesc(source)}), in target ${getNameOrDesc(target)}`; super(target, concern, reason, options); configureCustomError(this); From 11fdc16798b4ca851beaffa6f161473a506e86ff Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 12:10:55 +0100 Subject: [PATCH 348/424] Add defineAliases() implementation --- .../support/src/concerns/ConcernsInjector.ts | 241 ++++++++++++++++-- 1 file changed, 214 insertions(+), 27 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 04415b28..acf75f6b 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -1,4 +1,4 @@ -import type { +import { ConcernConstructor, Injector, UsesConcerns, @@ -6,10 +6,13 @@ import type { Owner, Container, Factory, + Alias, + Aliases, } from "@aedart/contracts/support/concerns"; import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import { CONCERN_CLASSES, + ALIASES, CONCERNS } from "@aedart/contracts/support/concerns"; import { @@ -17,6 +20,7 @@ import { getNameOrDesc, getClassPropertyDescriptors, } from "@aedart/support/reflections"; +import AliasConflictError from './exceptions/AliasConflictError'; import AlreadyRegisteredError from './exceptions/AlreadyRegisteredError'; import InjectionError from './exceptions/InjectionError'; import UnsafeAliasError from './exceptions/UnsafeAliasError'; @@ -195,32 +199,57 @@ export default class ConcernsInjector implements Injector return target; } - // /** - // * Defines "aliases" (proxy properties and methods) in target class' prototype, such that they - // * point to the properties and methods available in the concern classes. - // * - // * **Note**: _Method defines each alias using the {@link defineAlias} method!_ - // * - // * @template T = object - // * - // * @param {UsesConcerns} target The target in which "aliases" must be defined in - // * @param {Configuration[]} configurations List of concern injection configurations - // * - // * @returns {UsesConcerns} The modified target class - // * - // * @throws {AliasConflictException} If case of alias naming conflicts. - // * @throws {InjectionException} If unable to define aliases in target class. - // */ - // defineAliases(target: UsesConcerns, configurations: Configuration[]): UsesConcerns - // { - // // TODO: implement this method... - // // TODO: cache target property descriptors - // // TODO: cache concern property descriptors - // // TODO: - delete concern property descriptors after its aliases are defined - // // TODO: clear all cached descriptors, after all aliases defined - // - // return target; - // } + /** + * Defines "aliases" (proxy properties and methods) in target class' prototype, such that they + * point to the properties and methods available in the concern classes. + * + * **Note**: _Method defines each alias using the {@link defineAlias} method!_ + * + * @template T = object + * + * @param {UsesConcerns} target The target in which "aliases" must be defined in + * @param {Configuration[]} configurations List of concern injection configurations + * + * @returns {UsesConcerns} The modified target class + * + * @throws {AliasConflictError} If case of alias naming conflicts. + * @throws {InjectionError} If unable to define aliases in target class. + */ + defineAliases(target: UsesConcerns, configurations: Configuration[]): UsesConcerns + { + const applied: Alias[] = []; + + // Obtain previous applied aliases, form the target's parents. + const appliedByParents: Map = this.getAllAppliedAliases(target as UsesConcerns); + + this.cacheDescriptorsDuring(target, () => { + for (const configuration of configurations) { + if (!configuration.allowAliases) { + continue; + } + + this.cacheDescriptorsDuring(configuration.concern, () => { + // Process the configuration aliases and define them. Merge returned aliases with the + // applied aliases for the target class. + const newApplied: Alias[] = this.processAliases( + target as UsesConcerns, + configuration, + applied, + appliedByParents + ); + + applied.push(...newApplied); + }); + } + }); + + // (Re)define the "ALIASES" static property in target. + return this.definePropertyInTarget(target, ALIASES, { + get: function() { + return applied; + } + }); + } /** * Defines an "alias" (proxy property or method) in target class' prototype, which points to a property or method @@ -408,6 +437,123 @@ export default class ConcernsInjector implements Injector return null; } + /** + * Returns all applied aliases for given target and its parent classes + * + * @param {UsesConcerns} target + * @param {boolean} [includeTarget=false] + * + * @return {Map} + * + * @protected + */ + protected getAllAppliedAliases(target: UsesConcerns, includeTarget: boolean = false): Map + { + const output: Map = new Map(); + + const parents = getAllParentsOfClass(target as ConstructorOrAbstractConstructor, includeTarget).reverse(); + for (const parent of parents) { + if (!Reflect.has(parent, ALIASES)) { + continue; + } + + (parent as UsesConcerns)[ALIASES].forEach((alias) => { + output.set(alias, (parent as UsesConcerns)); + }); + } + + return output; + } + + /** + * Processes given configuration's aliases by defining them + * + * @param {UsesConcerns} target + * @param {Configuration} configuration + * @param {Alias[]} applied + * @param {Map} appliedByParents + * + * @return {Alias[]} New applied aliases (does not include aliases from `applied` argument) + * + * @protected + * + * @throws {AliasConflictError} + */ + protected processAliases( + target: UsesConcerns, + configuration: Configuration, + applied: Alias[], + appliedByParents: Map + ): Alias[] + { + // Aliases that have been applied by this method... + const output: Alias[] = []; + + // Already applied aliases in target + aliases applied by this method + const alreadyApplied: Alias[] = [...applied]; + + const aliases: Aliases = configuration.aliases as Aliases; + const properties: PropertyKey[] = Reflect.ownKeys(aliases); + + for (const key of properties) { + // @ts-expect-error Alias is obtained correctly here... + const alias: Alias = aliases[key] as Alias; + + // Ensure that alias does not conflict with previous applied aliases. + this.assertAliasDoesNotConflict( + target as UsesConcerns, + configuration.concern, + alias, + key, + alreadyApplied, // Applied aliases in this context... + appliedByParents + ); + + // Define the alias in target and mark it as "applied" + this.defineAlias(target, alias, key, configuration.concern); + + alreadyApplied.push(alias); + output.push(alias); + } + + return output; + } + + /** + * Assert that given alias does not conflict with an already applied alias + * + * @param {UsesConcerns} target + * @param {ConcernConstructor} concern + * @param {Alias} alias + * @param {PropertyKey} key + * @param {Alias} applied Aliases that are applied directly in the target class + * @param {Map} appliedByParents Aliases that are applied in target's parents + * + * @protected + * + * @throws {AliasConflictError} + */ + protected assertAliasDoesNotConflict( + target: UsesConcerns, + concern: ConcernConstructor, + alias: Alias, + key: PropertyKey, + applied: Alias[], + appliedByParents: Map + ): void + { + const isAppliedByTarget: boolean = applied.includes(alias); + const isAppliedByParents: boolean = appliedByParents.has(alias); + + if (isAppliedByTarget || isAppliedByParents) { + const source: UsesConcerns = (isAppliedByTarget) + ? target as UsesConcerns + : appliedByParents.get(alias) as UsesConcerns; + + throw new AliasConflictError(target, concern, alias, key, source); + } + } + /** * Resolves the proxy property descriptor for given key in source concern * @@ -517,6 +663,47 @@ export default class ConcernsInjector implements Injector } } + /** + * Caches property descriptors during the given callback. + * Once the callback has been invoked, the cached descriptors are deleted again + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target + * @param {() => any} callback + * + * @return {any} Callback's return value, if any + * + * @protected + */ + protected cacheDescriptorsDuring( + target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, + callback: () => any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + this.cacheDescriptorsFor(target); + + const output = callback(); + + this.deleteCachedDescriptorsFor(target); + + return output; + } + + /** + * Retrieves the property descriptors for given target and caches them + * + * @see getDescriptorsFor + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * + * @returns {Record} + * + * @protected + */ + protected cacheDescriptorsFor(target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor): Record + { + return this.getDescriptorsFor(target, true, true); + } + /** * Returns property descriptors for given target class (recursively) * From df4c5ba52e3d4ca199b60e0e3b6c46cd247fefff Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 12:11:09 +0100 Subject: [PATCH 349/424] Add tests for defineAliases() --- .../concerns/injector/defineAliases.test.js | 386 ++++++++++++++++++ 1 file changed, 386 insertions(+) create mode 100644 tests/browser/packages/support/concerns/injector/defineAliases.test.js diff --git a/tests/browser/packages/support/concerns/injector/defineAliases.test.js b/tests/browser/packages/support/concerns/injector/defineAliases.test.js new file mode 100644 index 00000000..7c252705 --- /dev/null +++ b/tests/browser/packages/support/concerns/injector/defineAliases.test.js @@ -0,0 +1,386 @@ +import makeConcernsInjector from "../helpers/makeConcernsInjector"; +import { ALIASES } from "@aedart/contracts/support/concerns"; +import { AbstractConcern, AliasConflictError } from "@aedart/support/concerns"; + +fdescribe('@aedart/support/concerns', () => { + describe('ConcernsInjector', () => { + describe('defineAliases()', () => { + + it('defines default aliases in target', () => { + + class ConcernA extends AbstractConcern { + get foo() { + return 'bar'; + } + + greetings(name) { + return `Hi ${name}`; + } + } + + /** + * @property {(name: string) => string} greetings + * @property {string} foo + */ + class A {} + + const concerns = [ ConcernA ]; + + const injector = makeConcernsInjector(A); + const target = injector.defineContainer( + injector.defineConcerns(A, concerns) + ); + + const configurations = injector.normalise(concerns); + injector.defineAliases(target, configurations); + + // --------------------------------------------------------------------------- // + + expect(Reflect.has(A, ALIASES)) + .withContext('ALIASES static property not defined') + .toBeTrue(); + + expect(A[ALIASES]) + .withContext('Incorrect aliases defined in static ALIASES property') + .toEqual([ 'foo', 'greetings' ]); + + // --------------------------------------------------------------------------- // + + const instance = new A(); + expect(instance.foo) + .withContext('Interaction with alias "foo" failed') + .toBe('bar'); + + expect(instance.greetings('John')) + .withContext('Interaction with alias "greetings" failed') + .toBe('Hi John'); + }); + + it('can define custom aliases in target', () => { + + class ConcernA extends AbstractConcern { + get foo() { + return 'bar'; + } + + greetings(name) { + return `Hi ${name}`; + } + } + + /** + * @property {(name: string) => string} sayHi + * @property {string} ping + */ + class A {} + + const concerns = [ ConcernA ]; + + const injector = makeConcernsInjector(A); + const target = injector.defineContainer( + injector.defineConcerns(A, concerns) + ); + + const configurations = injector.normalise([ + { + concern: ConcernA, + aliases: { + 'foo': 'ping', + 'greetings': 'sayHi' + } + } + ]); + injector.defineAliases(target, configurations); + + // --------------------------------------------------------------------------- // + + expect(A[ALIASES]) + .withContext('Incorrect aliases defined in static ALIASES property') + .toEqual([ 'ping', 'sayHi' ]); + + // --------------------------------------------------------------------------- // + + const instance = new A(); + expect(instance.ping) + .withContext('Interaction with alias "ping" failed') + .toBe('bar'); + + expect(instance.sayHi('Siw')) + .withContext('Interaction with alias "sayHi" failed') + .toBe('Hi Siw'); + }); + + it('does not define aliases if "allowAliases" setting set to false', () => { + + class ConcernA extends AbstractConcern { + get foo() {} + greetings() {} + } + + class A {} + + const concerns = [ ConcernA ]; + + const injector = makeConcernsInjector(A); + const target = injector.defineContainer( + injector.defineConcerns(A, concerns) + ); + + const configurations = injector.normalise([ + { + concern: ConcernA, + allowAliases: false + } + ]); + injector.defineAliases(target, configurations); + + // --------------------------------------------------------------------------- // + + expect(A[ALIASES].length) + .withContext('static ALIASES property should be empty') + .toBe(0); + }); + + it('conflicts when same alias is used multiple times', () => { + + class ConcernA extends AbstractConcern { + get foo() {} + greetings() {} + } + + class A {} + + const concerns = [ ConcernA ]; + + const injector = makeConcernsInjector(A); + const target = injector.defineContainer( + injector.defineConcerns(A, concerns) + ); + + const configurations = injector.normalise([ + { + concern: ConcernA, + aliases: { + 'foo': 'bar', + 'greetings': 'bar', // This should result in alias conflict + } + } + ]); + + // --------------------------------------------------------------------------- // + + const callback = () => { + injector.defineAliases(target, configurations); + } + + // Debug + // console.log('A aliases', A[ALIASES]); + // console.log('A keys', Reflect.ownKeys(A.prototype)); + + expect(callback) + .toThrowError(AliasConflictError); + }); + + it('conflicts when default applied alias is already applied by another concern', () => { + + class ConcernA extends AbstractConcern { + get foo() {} + greetings() {} + } + + class ConcernB extends AbstractConcern { + greetings() {} // This will result in a conflict + } + + class A {} + + const concerns = [ ConcernA, ConcernB ]; + + const injector = makeConcernsInjector(A); + const target = injector.defineContainer( + injector.defineConcerns(A, concerns) + ); + + const configurations = injector.normalise([ + ConcernA, + ConcernB + ]); + + // --------------------------------------------------------------------------- // + + const callback = () => { + injector.defineAliases(target, configurations); + } + + expect(callback) + .toThrowError(AliasConflictError); + }); + + it('inherits previous applied aliases from parent class', () => { + + class ConcernA extends AbstractConcern { + get ping() { + return 'ping'; + } + } + + class ConcernB extends AbstractConcern { + get pong() { + return 'pong'; + } + } + + /** + * @property {string} ping + */ + class A {} + + /** + * @property {string} pong + */ + class B extends A {} + + const concernsA = [ ConcernA ]; + const concernsB = [ ConcernB ]; + + const injectorA = makeConcernsInjector(A); + const targetA = injectorA.defineContainer( + injectorA.defineConcerns(A, concernsA) + ); + + const injectorB = makeConcernsInjector(B); + const targetB = injectorB.defineContainer( + injectorB.defineConcerns(B, concernsB) + ); + + injectorA.defineAliases(targetA, injectorA.normalise(concernsA)); + injectorB.defineAliases(targetB, injectorB.normalise(concernsB)); + + // --------------------------------------------------------------------------- // + + // Debug + // console.log('A', A[ALIASES]); + // console.log('B', B[ALIASES]); + + expect(A[ALIASES]) + .withContext('Incorrect aliases defined A') + .toEqual([ 'ping' ]); + + expect(B[ALIASES]) + .withContext('Incorrect aliases defined A') + .toEqual([ 'pong' ]); + + // --------------------------------------------------------------------------- // + + const instance = new B(); + expect(instance.ping) + .withContext('Interaction with alias "ping" failed') + .toBe('ping'); + + expect(instance.pong) + .withContext('Interaction with alias "pong" failed') + .toBe('pong'); + }); + + it('conflicts when alias already defined in parent class', () => { + + class ConcernA extends AbstractConcern { + get ping() {} + } + + class ConcernB extends AbstractConcern { + get ping() {} // Will result in conflict + } + + + class A {} + class B extends A {} + + const concernsA = [ ConcernA ]; + const concernsB = [ ConcernB ]; + + const injectorA = makeConcernsInjector(A); + const targetA = injectorA.defineContainer( + injectorA.defineConcerns(A, concernsA) + ); + + const injectorB = makeConcernsInjector(B); + const targetB = injectorB.defineContainer( + injectorB.defineConcerns(B, concernsB) + ); + + // --------------------------------------------------------------------------- // + + injectorA.defineAliases(targetA, injectorA.normalise(concernsA)); + + const callback = () => { + injectorB.defineAliases(targetB, injectorB.normalise(concernsB)); + } + + expect(callback) + .toThrowError(AliasConflictError); + }); + + it('can prevent conflict via a custom alias in subclass', () => { + + class ConcernA extends AbstractConcern { + get ping() { + return 'ping' + } + } + + class ConcernB extends AbstractConcern { + get ping() { + return 'pong' + } + } + + /** + * @property {string} ping + */ + class A {} + + /** + * @property {string} pong + */ + class B extends A {} + + const concernsA = [ ConcernA ]; + const concernsB = [ ConcernB ]; + + const injectorA = makeConcernsInjector(A); + const targetA = injectorA.defineContainer( + injectorA.defineConcerns(A, concernsA) + ); + + const injectorB = makeConcernsInjector(B); + const targetB = injectorB.defineContainer( + injectorB.defineConcerns(B, concernsB) + ); + + // --------------------------------------------------------------------------- // + + injectorA.defineAliases(targetA, injectorA.normalise(concernsA)); + injectorB.defineAliases(targetB, injectorB.normalise([ + { + concern: ConcernB, + aliases: { + 'ping': 'pong' // This should prevent alias conflict + } + } + ])); + + // --------------------------------------------------------------------------- // + + const instance = new B(); + expect(instance.ping) + .withContext('Interaction with alias "ping" failed') + .toBe('ping'); + + expect(instance.pong) + .withContext('Interaction with alias "pong" failed') + .toBe('pong'); + }); + }); + }); +}); \ No newline at end of file From 0a84ca8bbea927c40f647b555a57122f9b442a3a Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 12:27:02 +0100 Subject: [PATCH 350/424] Cleanup --- .../packages/support/concerns/injector/defineAliases.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/packages/support/concerns/injector/defineAliases.test.js b/tests/browser/packages/support/concerns/injector/defineAliases.test.js index 7c252705..33e91b62 100644 --- a/tests/browser/packages/support/concerns/injector/defineAliases.test.js +++ b/tests/browser/packages/support/concerns/injector/defineAliases.test.js @@ -2,7 +2,7 @@ import makeConcernsInjector from "../helpers/makeConcernsInjector"; import { ALIASES } from "@aedart/contracts/support/concerns"; import { AbstractConcern, AliasConflictError } from "@aedart/support/concerns"; -fdescribe('@aedart/support/concerns', () => { +describe('@aedart/support/concerns', () => { describe('ConcernsInjector', () => { describe('defineAliases()', () => { From 4b922d51bf04e64553e843dc9422e484cdf5df3f Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 14:30:03 +0100 Subject: [PATCH 351/424] Add Descriptors Cache util --- .../src/support/concerns/DescriptorsCache.ts | 67 ++++++++++ .../contracts/src/support/concerns/index.ts | 2 + packages/support/src/concerns/Descriptors.ts | 118 ++++++++++++++++++ packages/support/src/concerns/index.ts | 4 +- 4 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 packages/contracts/src/support/concerns/DescriptorsCache.ts create mode 100644 packages/support/src/concerns/Descriptors.ts diff --git a/packages/contracts/src/support/concerns/DescriptorsCache.ts b/packages/contracts/src/support/concerns/DescriptorsCache.ts new file mode 100644 index 00000000..307734d1 --- /dev/null +++ b/packages/contracts/src/support/concerns/DescriptorsCache.ts @@ -0,0 +1,67 @@ +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import ConcernConstructor from './ConcernConstructor'; +import UsesConcerns from './UsesConcerns'; + +/** + * Descriptors Cache + * + * Utility for obtaining property descriptors for a target class or concern. + */ +export default interface DescriptorsCache +{ + /** + * Caches property descriptors for target during the execution of callback. + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {() => any} callback Callback to invoke + * @param {boolean} [forgetAfter=true] It `true`, cached descriptors are deleted after callback is invoked + * + * @return {any} + */ + rememberDuring( + target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, + callback: () => any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + forgetAfter?: boolean + ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + /** + * Retrieves the property descriptors for given target and caches them + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {boolean} [force=false] If `true` then evt. previous cached result is not used. + * + * @returns {Record} + */ + remember(target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, force?: boolean): Record; + + /** + * Returns property descriptors for given target class (recursively) + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {boolean} [force=false] If `true` then method will not return evt. cached descriptors. + * @param {boolean} [cache=false] Caches the descriptors if `true`. + * + * @returns {Record} + */ + get( + target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, + force?: boolean, + cache?: boolean + ): Record; + + /** + * Deletes cached descriptors for target + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target + * + * @return {boolean} + */ + forget(target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor): boolean; + + /** + * Clears all cached descriptors + * + * @return {this} + */ + clear(): this +} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 915ca3a6..3cc5f148 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -60,6 +60,7 @@ import Concern from "./Concern"; import ConcernConstructor from "./ConcernConstructor"; import Configuration from "./Configuration"; import Container from "./Container"; +import DescriptorsCache from "./DescriptorsCache"; import Factory from "./Factory"; import Injector from "./Injector"; import Owner from "./Owner"; @@ -69,6 +70,7 @@ export { type ConcernConstructor, type Configuration, type Container, + type DescriptorsCache, type Factory, type Injector, type Owner, diff --git a/packages/support/src/concerns/Descriptors.ts b/packages/support/src/concerns/Descriptors.ts new file mode 100644 index 00000000..fef4f976 --- /dev/null +++ b/packages/support/src/concerns/Descriptors.ts @@ -0,0 +1,118 @@ +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConcernConstructor, UsesConcerns, DescriptorsCache } from "@aedart/contracts/support/concerns"; +import { getClassPropertyDescriptors } from "@aedart/support/reflections"; + +/** + * Descriptors + * + * @see DescriptorsCache + */ +export default class Descriptors implements DescriptorsCache +{ + /** + * In-memory cache property descriptors for target class and concern classes + * + * @type {WeakMap>} + * + * @private + */ + #cached: WeakMap< + ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, + Record + >; + + /** + * Create new Descriptors instance + */ + constructor() { + this.#cached = new WeakMap(); + } + + /** + * Caches property descriptors for target during the execution of callback. + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {() => any} callback Callback to invoke + * @param {boolean} [forgetAfter=true] It `true`, cached descriptors are deleted after callback is invoked + */ + public rememberDuring( + target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, + callback: () => any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + forgetAfter: boolean = true + ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + this.remember(target); + + const output = callback(); + + if (forgetAfter) { + this.forget(target); + } + + return output; + } + + /** + * Retrieves the property descriptors for given target and caches them + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {boolean} [force=false] If `true` then evt. previous cached result is not used. + * + * @returns {Record} + */ + public remember(target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, force: boolean = false): Record + { + return this.get(target, force, true); + } + + /** + * Returns property descriptors for given target class (recursively) + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {boolean} [force=false] If `true` then method will not return evt. cached descriptors. + * @param {boolean} [cache=false] Caches the descriptors if `true`. + * + * @returns {Record} + */ + public get( + target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, + force: boolean = false, + cache: boolean = false + ): Record + { + if (!force && this.#cached.has(target)) { + return this.#cached.get(target) as Record; + } + + const descriptors = getClassPropertyDescriptors(target, true); + if (cache) { + this.#cached.set(target, descriptors); + } + + return descriptors; + } + + /** + * Deletes cached descriptors for target + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target + * + * @return {boolean} + */ + public forget(target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor): boolean + { + return this.#cached.delete(target); + } + + /** + * Clears all cached descriptors + * + * @return {this} + */ + public clear(): this + { + this.#cached = new WeakMap(); + + return this; + } +} \ No newline at end of file diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 2d78e0f9..cca3f9bf 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -3,13 +3,15 @@ import { ConcernClassBlueprint } from "./ConcernClassBlueprint"; import ConcernsContainer from "./ConcernsContainer"; import ConcernsInjector from "./ConcernsInjector"; import ConfigurationFactory from "./ConfigurationFactory"; +import Descriptors from "./Descriptors"; export { AbstractConcern, ConcernClassBlueprint, ConcernsContainer, ConcernsInjector, - ConfigurationFactory + ConfigurationFactory, + Descriptors }; export * from './exceptions'; From 530a8b8b07e530cf007ec70a9de073c1fcbf3eb8 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 14:33:59 +0100 Subject: [PATCH 352/424] Refactor Injector, use descriptors cache util --- .../support/src/concerns/ConcernsInjector.ts | 118 +++--------------- 1 file changed, 19 insertions(+), 99 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index acf75f6b..aec435ec 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -5,6 +5,7 @@ import { Configuration, Owner, Container, + DescriptorsCache, Factory, Alias, Aliases, @@ -26,6 +27,7 @@ import InjectionError from './exceptions/InjectionError'; import UnsafeAliasError from './exceptions/UnsafeAliasError'; import ConcernsContainer from './ConcernsContainer'; import ConfigurationFactory from "./ConfigurationFactory"; +import Descriptors from "./Descriptors"; import { isUnsafeKey } from "./isUnsafeKey"; /** @@ -66,16 +68,13 @@ export default class ConcernsInjector implements Injector protected factory: Factory; /** - * In-memory cache property descriptors for target class and concern classes - * - * @type {WeakMap>} - * - * @private + * Descriptors Cache + * + * @type {DescriptorsCache} + * + * @protected */ - #cachedDescriptors: WeakMap< - ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, - Record - > = new WeakMap(); + protected descriptors: DescriptorsCache; /** * Create a new Concerns Injector instance @@ -88,6 +87,7 @@ export default class ConcernsInjector implements Injector { this.#target = target; this.factory = this.makeConfigurationFactory(); + this.descriptors = this.makeDescriptorsCache(); } /** @@ -134,6 +134,8 @@ export default class ConcernsInjector implements Injector // // B) Define a concerns container in target class' prototype // // // C) Define "aliases" (proxy properties and methods) in target class' prototype + // + // // TODO: Clear all cached descriptors // // return this.target as UsesConcerns; // } @@ -222,13 +224,13 @@ export default class ConcernsInjector implements Injector // Obtain previous applied aliases, form the target's parents. const appliedByParents: Map = this.getAllAppliedAliases(target as UsesConcerns); - this.cacheDescriptorsDuring(target, () => { + this.descriptors.rememberDuring(target, () => { for (const configuration of configurations) { if (!configuration.allowAliases) { continue; } - this.cacheDescriptorsDuring(configuration.concern, () => { + this.descriptors.rememberDuring(configuration.concern, () => { // Process the configuration aliases and define them. Merge returned aliases with the // applied aliases for the target class. const newApplied: Alias[] = this.processAliases( @@ -284,13 +286,13 @@ export default class ConcernsInjector implements Injector } // Skip if a property key already exists with same name as the "alias" - const targetDescriptors = this.getDescriptorsFor(target); + const targetDescriptors = this.descriptors.get(target); if (Reflect.has(targetDescriptors, alias)) { return false; } // Abort if unable to find descriptor that matches given key in concern class. - const concernDescriptors = this.getDescriptorsFor(source); + const concernDescriptors = this.descriptors.get(source); if (!Reflect.has(concernDescriptors, key)) { throw new InjectionError(target, source, `"${key.toString()}" does not exist in concern ${getNameOrDesc(source)} - attempted aliased as "${alias.toString()}" in target ${getNameOrDesc(target)}`); } @@ -664,97 +666,15 @@ export default class ConcernsInjector implements Injector } /** - * Caches property descriptors during the given callback. - * Once the callback has been invoked, the cached descriptors are deleted again - * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target - * @param {() => any} callback - * - * @return {any} Callback's return value, if any - * - * @protected - */ - protected cacheDescriptorsDuring( - target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, - callback: () => any /* eslint-disable-line @typescript-eslint/no-explicit-any */ - ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ - { - this.cacheDescriptorsFor(target); - - const output = callback(); - - this.deleteCachedDescriptorsFor(target); - - return output; - } - - /** - * Retrieves the property descriptors for given target and caches them + * Returns a new Descriptors (cache) instance * - * @see getDescriptorsFor - * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class - * - * @returns {Record} + * @return {DescriptorsCache} * * @protected */ - protected cacheDescriptorsFor(target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor): Record - { - return this.getDescriptorsFor(target, true, true); - } - - /** - * Returns property descriptors for given target class (recursively) - * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class - * @param {boolean} [force=false] If `true` then method will not return evt. cached descriptors. - * @param {boolean} [cache=false] Caches the descriptors if `true`. - * - * @returns {Record} - * - * @protected - */ - protected getDescriptorsFor( - target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, - force: boolean = false, - cache: boolean = false - ): Record - { - if (!force && this.#cachedDescriptors.has(target)) { - return this.#cachedDescriptors.get(target) as Record; - } - - const descriptors = getClassPropertyDescriptors(target, true); - if (cache) { - this.#cachedDescriptors.set(target, descriptors); - } - - return descriptors; - } - - /** - * Deletes cached property descriptors for target - * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target - * - * @returns {boolean} `true` if cached descriptors were removed, `false` if none were cached - * - * @protected - */ - protected deleteCachedDescriptorsFor(target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor): boolean - { - return this.#cachedDescriptors.delete(target); - } - - /** - * Clears all cached property descriptors - * - * @protected - */ - protected clearCachedDescriptors(): void + protected makeDescriptorsCache(): DescriptorsCache { - this.#cachedDescriptors = new WeakMap(); + return new Descriptors(); } /** From f0290683d7a9923f7f0a218e36044721ae2bf86b Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 14:34:52 +0100 Subject: [PATCH 353/424] Cleanup --- packages/support/src/concerns/ConcernsInjector.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index aec435ec..f91a59f6 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -18,8 +18,7 @@ import { } from "@aedart/contracts/support/concerns"; import { getAllParentsOfClass, - getNameOrDesc, - getClassPropertyDescriptors, + getNameOrDesc } from "@aedart/support/reflections"; import AliasConflictError from './exceptions/AliasConflictError'; import AlreadyRegisteredError from './exceptions/AlreadyRegisteredError'; From 3ee7485976f85443b660ff087e3a9062688ad50d Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 15:01:54 +0100 Subject: [PATCH 354/424] Add Proxy Property Descriptor Resolver --- .../src/support/concerns/Resolver.ts | 18 +++ .../contracts/src/support/concerns/index.ts | 2 + .../support/src/concerns/ProxyResolver.ts | 121 ++++++++++++++++++ packages/support/src/concerns/index.ts | 4 +- 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 packages/contracts/src/support/concerns/Resolver.ts create mode 100644 packages/support/src/concerns/ProxyResolver.ts diff --git a/packages/contracts/src/support/concerns/Resolver.ts b/packages/contracts/src/support/concerns/Resolver.ts new file mode 100644 index 00000000..72a099d9 --- /dev/null +++ b/packages/contracts/src/support/concerns/Resolver.ts @@ -0,0 +1,18 @@ +import ConcernConstructor from './ConcernConstructor'; + +/** + * Proxy Descriptor Resolver + */ +export default interface Resolver +{ + /** + * Returns a property descriptor to be used for an "alias" property or method in a target class + * + * @param {PropertyKey} key The property key in `source` concern + * @param {ConcernConstructor} source The concern that holds the property key + * @param {PropertyDescriptor} keyDescriptor Descriptor of `key` in `source` + * + * @returns {PropertyDescriptor} Descriptor to be used for defining alias in a target class + */ + resolve(key: PropertyKey, source: ConcernConstructor, keyDescriptor: PropertyDescriptor): PropertyDescriptor; +} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 3cc5f148..e5a99a21 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -64,6 +64,7 @@ import DescriptorsCache from "./DescriptorsCache"; import Factory from "./Factory"; import Injector from "./Injector"; import Owner from "./Owner"; +import Resolver from "./Resolver"; import UsesConcerns from "./UsesConcerns"; export { type Concern, @@ -74,6 +75,7 @@ export { type Factory, type Injector, type Owner, + type Resolver, type UsesConcerns } diff --git a/packages/support/src/concerns/ProxyResolver.ts b/packages/support/src/concerns/ProxyResolver.ts new file mode 100644 index 00000000..44fd1ab4 --- /dev/null +++ b/packages/support/src/concerns/ProxyResolver.ts @@ -0,0 +1,121 @@ +import type { + Resolver, + ConcernConstructor, + Owner +} from "@aedart/contracts/support/concerns"; +import { CONCERNS } from "@aedart/contracts/support/concerns"; + +/** + * Proxy Descriptor Resolver + * + * @see Resolver + */ +export default class ProxyResolver implements Resolver +{ + /** + * Returns a property descriptor to be used for an "alias" property or method in a target class + * + * @param {PropertyKey} key The property key in `source` concern + * @param {ConcernConstructor} source The concern that holds the property key + * @param {PropertyDescriptor} keyDescriptor Descriptor of `key` in `source` + * + * @returns {PropertyDescriptor} Descriptor to be used for defining alias in a target class + */ + resolve(key: PropertyKey, source: ConcernConstructor, keyDescriptor: PropertyDescriptor): PropertyDescriptor + { + const proxy: PropertyDescriptor = Object.assign(Object.create(null), { + configurable: keyDescriptor.configurable, + enumerable: keyDescriptor.enumerable, + // writable: keyDescriptor.writable // Do not specify here... + }); + + // A descriptor can only have an accessor, a value or writable attribute. Depending on the "value" + // a different kind of proxy must be defined. + const hasValue: boolean = Reflect.has(keyDescriptor, 'value'); + + if (hasValue && typeof keyDescriptor.value == 'function') { + proxy.value = this.makeMethodProxy(key, source); + } else if (hasValue) { + // When value is not a function, it could be a readonly property... + proxy.get = this.makeGetPropertyProxy(key, source); + + // However, if the descriptor claims that its writable, then + // a setter must be defined too. + if (keyDescriptor.writable) { + proxy.set = this.makeSetPropertyProxy(key, source); + } + } else { + // Otherwise, the property can a getter and or a setter... + if (Reflect.has(keyDescriptor, 'get')) { + proxy.get = this.makeGetPropertyProxy(key, source); + } + + if (Reflect.has(keyDescriptor, 'set')) { + proxy.set = this.makeSetPropertyProxy(key, source); + } + } + + return proxy; + } + + /** + * Returns a new proxy "method" for given method in this concern + * + * @param {PropertyKey} method + * @param {ConcernConstructor} concern + * + * @returns {(...args: any[]) => any} + * + * @protected + */ + protected makeMethodProxy(method: PropertyKey, concern: ConcernConstructor) + { + return function( + ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + // @ts-expect-error This = concern instance + return (this as Owner)[CONCERNS].call(concern, method, ...args); + } + } + + /** + * Returns a new proxy "get" for given property in this concern + * + * @param {PropertyKey} property + * @param {ConcernConstructor} concern + * + * @returns {() => any} + * + * @protected + */ + protected makeGetPropertyProxy(property: PropertyKey, concern: ConcernConstructor) + { + return function(): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + // @ts-expect-error This = concern instance + return (this as Owner)[CONCERNS].getProperty(concern, property); + } + } + + /** + * Returns a new proxy "set" for given property in this concern + * + * @param {PropertyKey} property + * @param {ConcernConstructor} concern + * + * @returns {(value: any) => void} + * + * @protected + */ + protected makeSetPropertyProxy(property: PropertyKey, concern: ConcernConstructor) + { + return function( + value: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): void + { + // @ts-expect-error This = concern instance + (this as Owner)[CONCERNS].setProperty(concern, property, value); + } + } +} \ No newline at end of file diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index cca3f9bf..06a4b02e 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -4,6 +4,7 @@ import ConcernsContainer from "./ConcernsContainer"; import ConcernsInjector from "./ConcernsInjector"; import ConfigurationFactory from "./ConfigurationFactory"; import Descriptors from "./Descriptors"; +import ProxyResolver from "./ProxyResolver"; export { AbstractConcern, @@ -11,7 +12,8 @@ export { ConcernsContainer, ConcernsInjector, ConfigurationFactory, - Descriptors + Descriptors, + ProxyResolver }; export * from './exceptions'; From 9e863242d9c76920f5dd544e50f93eb3ed934bc0 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 15:02:25 +0100 Subject: [PATCH 355/424] Refactor, use new Proxy Descriptor Resolver --- .../support/src/concerns/ConcernsInjector.ts | 133 ++++-------------- 1 file changed, 24 insertions(+), 109 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index f91a59f6..b79a25d8 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -9,6 +9,7 @@ import { Factory, Alias, Aliases, + Resolver, } from "@aedart/contracts/support/concerns"; import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import { @@ -28,6 +29,7 @@ import ConcernsContainer from './ConcernsContainer'; import ConfigurationFactory from "./ConfigurationFactory"; import Descriptors from "./Descriptors"; import { isUnsafeKey } from "./isUnsafeKey"; +import ProxyResolver from "./ProxyResolver"; /** * A map of the concern owner instances and their concerns container @@ -74,6 +76,15 @@ export default class ConcernsInjector implements Injector * @protected */ protected descriptors: DescriptorsCache; + + /** + * Proxy Descriptor Resolver + * + * @type {Resolver} + * + * @protected + */ + protected proxyResolver: Resolver; /** * Create a new Concerns Injector instance @@ -87,6 +98,7 @@ export default class ConcernsInjector implements Injector this.#target = target; this.factory = this.makeConfigurationFactory(); this.descriptors = this.makeDescriptorsCache(); + this.proxyResolver = this.makeProxyResolver(); } /** @@ -297,7 +309,7 @@ export default class ConcernsInjector implements Injector } // Define the proxy property or method, using the concern's property descriptor to determine what must be defined. - const proxy = this.resolveProxyDescriptor(key, source, concernDescriptors[key]) + const proxy = this.proxyResolver.resolve(key, source, concernDescriptors[key]); return this.definePropertyInTarget(target.prototype, alias, proxy) !== undefined; } @@ -554,128 +566,31 @@ export default class ConcernsInjector implements Injector throw new AliasConflictError(target, concern, alias, key, source); } } - - /** - * Resolves the proxy property descriptor for given key in source concern - * - * @param {PropertyKey} key - * @param {ConcernConstructor} source - * @param {PropertyDescriptor} keyDescriptor Descriptor of `key` in `source` - * - * @returns {PropertyDescriptor} Descriptor to be used for defining alias in a target class - * - * @protected - */ - protected resolveProxyDescriptor(key: PropertyKey, source: ConcernConstructor, keyDescriptor: PropertyDescriptor): PropertyDescriptor - { - const proxy: PropertyDescriptor = Object.assign(Object.create(null), { - configurable: keyDescriptor.configurable, - enumerable: keyDescriptor.enumerable, - // writable: keyDescriptor.writable // Do not specify here... - }); - - // A descriptor can only have an accessor, a value or writable attribute. Depending on the "value" - // a different kind of proxy must be defined. - const hasValue: boolean = Reflect.has(keyDescriptor, 'value'); - if (hasValue && typeof keyDescriptor.value == 'function') { - proxy.value = this.makeMethodProxy(key, source); - } else if (hasValue) { - // When value is not a function, it could be a readonly property... - proxy.get = this.makeGetPropertyProxy(key, source); - - // However, if the descriptor claims that its writable, then - // a setter must be defined too. - if (keyDescriptor.writable) { - proxy.set = this.makeSetPropertyProxy(key, source); - } - } else { - // Otherwise, the property can a getter and or a setter... - if (Reflect.has(keyDescriptor, 'get')) { - proxy.get = this.makeGetPropertyProxy(key, source); - } - - if (Reflect.has(keyDescriptor, 'set')) { - proxy.set = this.makeSetPropertyProxy(key, source); - } - } - - return proxy; - } - /** - * Returns a new proxy "method" for given method in this concern - * - * @param {PropertyKey} method - * @param {ConcernConstructor} concern - * - * @returns {(...args: any[]) => any} - * - * @protected - */ - protected makeMethodProxy(method: PropertyKey, concern: ConcernConstructor) - { - return function( - ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ - ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ - { - // @ts-expect-error This = concern instance - return (this as Owner)[CONCERNS].call(concern, method, ...args); - } - } - - /** - * Returns a new proxy "get" for given property in this concern - * - * @param {PropertyKey} property - * @param {ConcernConstructor} concern - * - * @returns {() => any} - * - * @protected - */ - protected makeGetPropertyProxy(property: PropertyKey, concern: ConcernConstructor) - { - return function(): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ - { - // @ts-expect-error This = concern instance - return (this as Owner)[CONCERNS].getProperty(concern, property); - } - } - - /** - * Returns a new proxy "set" for given property in this concern - * - * @param {PropertyKey} property - * @param {ConcernConstructor} concern - * - * @returns {(value: any) => void} - * + * Returns a new Descriptors (cache) instance + * + * @return {DescriptorsCache} + * * @protected */ - protected makeSetPropertyProxy(property: PropertyKey, concern: ConcernConstructor) + protected makeDescriptorsCache(): DescriptorsCache { - return function( - value: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ - ): void - { - // @ts-expect-error This = concern instance - (this as Owner)[CONCERNS].setProperty(concern, property, value); - } + return new Descriptors(); } /** - * Returns a new Descriptors (cache) instance + * Returns a new Proxy Descriptor Resolver * - * @return {DescriptorsCache} + * @return {Resolver} * * @protected */ - protected makeDescriptorsCache(): DescriptorsCache + protected makeProxyResolver(): Resolver { - return new Descriptors(); + return new ProxyResolver(); } - + /** * Returns a new concern configuration factory instance * From ddb7bcfd50af5163ac7687a1a3370d7f6af7b0b0 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 15:06:29 +0100 Subject: [PATCH 356/424] Refactor, support custom config. factory, resolver, descriptors cache --- .../support/src/concerns/ConcernsInjector.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index b79a25d8..36916905 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -85,20 +85,28 @@ export default class ConcernsInjector implements Injector * @protected */ protected proxyResolver: Resolver; - + /** * Create a new Concerns Injector instance - * + * * @template T = object * * @param {T} target The target class that concerns must be injected into + * @param {Factory} [factory] + * @param {Resolver} [resolver] + * @param {DescriptorsCache} [descriptors] */ - public constructor(target: T) + public constructor( + target: T, + factory?: Factory, + resolver?: Resolver, + descriptors?: DescriptorsCache + ) { this.#target = target; - this.factory = this.makeConfigurationFactory(); - this.descriptors = this.makeDescriptorsCache(); - this.proxyResolver = this.makeProxyResolver(); + this.factory = factory || this.makeConfigurationFactory(); + this.proxyResolver = resolver || this.makeProxyResolver(); + this.descriptors = descriptors || this.makeDescriptorsCache(); } /** From b1135b917bfc6f3754815a9318865699552a0639 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 15:08:01 +0100 Subject: [PATCH 357/424] Rearrange internal methods --- .../support/src/concerns/ConcernsInjector.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 36916905..a27fbeb6 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -575,6 +575,20 @@ export default class ConcernsInjector implements Injector } } + /** + * Determine if key is "unsafe" + * + * @param {PropertyKey} key + * + * @returns {boolean} + * + * @protected + */ + protected isUnsafe(key: PropertyKey): boolean + { + return isUnsafeKey(key); + } + /** * Returns a new Descriptors (cache) instance * @@ -610,18 +624,4 @@ export default class ConcernsInjector implements Injector { return new ConfigurationFactory(); } - - /** - * Determine if key is "unsafe" - * - * @param {PropertyKey} key - * - * @returns {boolean} - * - * @protected - */ - protected isUnsafe(key: PropertyKey): boolean - { - return isUnsafeKey(key); - } } \ No newline at end of file From 9403f924619320b2272e5bb5745c14c18b302175 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 15:34:44 +0100 Subject: [PATCH 358/424] Add inject() implementation --- .../support/src/concerns/ConcernsInjector.ts | 75 +++++++++---------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index a27fbeb6..685818f7 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -120,44 +120,43 @@ export default class ConcernsInjector implements Injector { return this.#target; } - - // TODO: INCOMPLETE... - // - // /** - // * Injects concern classes into the target class and return the modified target. - // * - // * **Note**: _Method performs injection in the following way:_ - // * - // * _**A**: Defines the concern classes in target class, via {@link defineConcerns}._ - // * - // * _**B**: Defines a concerns container in target class' prototype, via {@link defineContainer}._ - // * - // * _**C**: Defines "aliases" (proxy properties and methods) in target class' prototype, via {@link defineAliases}._ - // * - // * @template T = object The target class that concern classes must be injected into - // * - // * @param {ConcernConstructor | Configuration} concerns List of concern classes / injection configurations - // * - // * @returns {UsesConcerns} The modified target class - // * - // * @throws {InjectionException} - // */ - // inject(...concerns: (ConcernConstructor|Configuration)[]): UsesConcerns; - // { - // // TODO: implement this method... - // - // // Resolve arguments, such that they are of type "concern injection configuration". - // - // // A) Define the concern classes in target class - // - // // B) Define a concerns container in target class' prototype - // - // // C) Define "aliases" (proxy properties and methods) in target class' prototype - // - // // TODO: Clear all cached descriptors - // - // return this.target as UsesConcerns; - // } + + /** + * Injects concern classes into the target class and return the modified target. + * + * **Note**: _Method performs injection in the following way:_ + * + * _**A**: Defines the concern classes in target class, via {@link defineConcerns}._ + * + * _**B**: Defines a concerns container in target class' prototype, via {@link defineContainer}._ + * + * _**C**: Defines "aliases" (proxy properties and methods) in target class' prototype, via {@link defineAliases}._ + * + * @template T = object The target class that concern classes must be injected into + * + * @param {ConcernConstructor | Configuration} concerns List of concern classes / injection configurations + * + * @returns {UsesConcerns} The modified target class + * + * @throws {InjectionException} + */ + inject(...concerns: (ConcernConstructor|Configuration)[]): UsesConcerns + { + const configurations: Configuration[] = this.normalise(concerns); + const concernClasses: ConcernConstructor[] = configurations.map((configuration) => configuration.concern); + + const modifiedTarget = this.defineAliases( + this.defineContainer( + this.defineConcerns(this.target, concernClasses) + ), + configurations + ); + + // Clear evt. cached descriptors... + this.descriptors.clear(); + + return modifiedTarget; + } /** * Defines the concern classes that must be used by the target class. From a311d355a095d0e20b382858e1d5d7db0373a0ae Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 15:36:21 +0100 Subject: [PATCH 359/424] Cleanup --- packages/support/src/concerns/ConcernsInjector.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 685818f7..acd6f36d 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -41,8 +41,6 @@ import ProxyResolver from "./ProxyResolver"; const CONTAINERS_REGISTRY: WeakMap = new WeakMap(); /** - * TODO: Incomplete - * * Concerns Injector * * @see Injector From 16bd59800dfe941103e83a670e93ece80c2e489f Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 26 Feb 2024 16:11:09 +0100 Subject: [PATCH 360/424] Remove redundant make methods --- .../support/src/concerns/ConcernsInjector.ts | 42 ++----------------- 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index acd6f36d..066ac568 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -102,9 +102,9 @@ export default class ConcernsInjector implements Injector ) { this.#target = target; - this.factory = factory || this.makeConfigurationFactory(); - this.proxyResolver = resolver || this.makeProxyResolver(); - this.descriptors = descriptors || this.makeDescriptorsCache(); + this.factory = factory || new ConfigurationFactory(); + this.proxyResolver = resolver || new ProxyResolver(); + this.descriptors = descriptors || new Descriptors(); } /** @@ -585,40 +585,4 @@ export default class ConcernsInjector implements Injector { return isUnsafeKey(key); } - - /** - * Returns a new Descriptors (cache) instance - * - * @return {DescriptorsCache} - * - * @protected - */ - protected makeDescriptorsCache(): DescriptorsCache - { - return new Descriptors(); - } - - /** - * Returns a new Proxy Descriptor Resolver - * - * @return {Resolver} - * - * @protected - */ - protected makeProxyResolver(): Resolver - { - return new ProxyResolver(); - } - - /** - * Returns a new concern configuration factory instance - * - * @returns {Factory} - * - * @protected - */ - protected makeConfigurationFactory(): Factory - { - return new ConfigurationFactory(); - } } \ No newline at end of file From eb71bc53e543d1b9b1cdef512bff3d9736a9dd47 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 08:31:28 +0100 Subject: [PATCH 361/424] Refactor, make resolve more readable --- .../support/src/concerns/ProxyResolver.ts | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/support/src/concerns/ProxyResolver.ts b/packages/support/src/concerns/ProxyResolver.ts index 44fd1ab4..e8107033 100644 --- a/packages/support/src/concerns/ProxyResolver.ts +++ b/packages/support/src/concerns/ProxyResolver.ts @@ -31,28 +31,33 @@ export default class ProxyResolver implements Resolver // A descriptor can only have an accessor, a value or writable attribute. Depending on the "value" // a different kind of proxy must be defined. - const hasValue: boolean = Reflect.has(keyDescriptor, 'value'); - if (hasValue && typeof keyDescriptor.value == 'function') { - proxy.value = this.makeMethodProxy(key, source); - } else if (hasValue) { - // When value is not a function, it could be a readonly property... + if (Reflect.has(keyDescriptor, 'value')) { + // When value is a method... + if (typeof keyDescriptor.value == 'function') { + proxy.value = this.makeMethodProxy(key, source); + return proxy; + } + + // Otherwise, when value isn't a method, it can be a readonly property... proxy.get = this.makeGetPropertyProxy(key, source); - // However, if the descriptor claims that its writable, then - // a setter must be defined too. + // But, if the descriptor claims that its writable, then a setter must + // also be defined. if (keyDescriptor.writable) { proxy.set = this.makeSetPropertyProxy(key, source); } - } else { - // Otherwise, the property can a getter and or a setter... - if (Reflect.has(keyDescriptor, 'get')) { - proxy.get = this.makeGetPropertyProxy(key, source); - } + + return proxy; + } - if (Reflect.has(keyDescriptor, 'set')) { - proxy.set = this.makeSetPropertyProxy(key, source); - } + // Otherwise, the property can a getter and or a setter... + if (Reflect.has(keyDescriptor, 'get')) { + proxy.get = this.makeGetPropertyProxy(key, source); + } + + if (Reflect.has(keyDescriptor, 'set')) { + proxy.set = this.makeSetPropertyProxy(key, source); } return proxy; From 8a947c3ff123f467fa2932813b327f85aa421ea7 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 09:46:02 +0100 Subject: [PATCH 362/424] Fix JSDoc --- packages/contracts/src/support/concerns/Injector.ts | 2 +- packages/support/src/concerns/ConcernsInjector.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index 70fa4171..40684dfa 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -34,7 +34,7 @@ export default interface Injector * * @template T = object The target class that concern classes must be injected into * - * @param {ConcernConstructor | Configuration} concerns List of concern classes / injection configurations + * @param {...ConcernConstructor | Configuration} concerns List of concern classes / injection configurations * * @returns {UsesConcerns} The modified target class * diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 066ac568..5b4be51b 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -132,7 +132,7 @@ export default class ConcernsInjector implements Injector * * @template T = object The target class that concern classes must be injected into * - * @param {ConcernConstructor | Configuration} concerns List of concern classes / injection configurations + * @param {...ConcernConstructor | Configuration} concerns List of concern classes / injection configurations * * @returns {UsesConcerns} The modified target class * From 912eff652f3b958f398ee56751c8a700cba4a057 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 10:25:50 +0100 Subject: [PATCH 363/424] Loosen the strict type for "Aliases" When defining a new concern injection configuration, it appears that me IDE has some notion that all of a Concern's properties must be declared, which is really strange. This loosens the aliases a bit and. --- packages/contracts/src/support/concerns/types.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/contracts/src/support/concerns/types.ts b/packages/contracts/src/support/concerns/types.ts index a3a2d3af..a5d225c4 100644 --- a/packages/contracts/src/support/concerns/types.ts +++ b/packages/contracts/src/support/concerns/types.ts @@ -13,6 +13,4 @@ export type Alias = PropertyKey; * class' prototype and acts as a proxy to the original property or method inside the * concern class instance. */ -export type Aliases = { - [K in keyof T]: Alias -}; \ No newline at end of file +export type Aliases = { [key in keyof T]: Alias } | { [key: PropertyKey]: Alias }; \ No newline at end of file From dcfc531dc5422e1dc6f332a041e5664cee74587e Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 10:32:03 +0100 Subject: [PATCH 364/424] Add test for Injector.inject() This should be sufficient to see that it works. All other methods have been tested at this point. --- .../support/concerns/injector/inject.test.js | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/browser/packages/support/concerns/injector/inject.test.js diff --git a/tests/browser/packages/support/concerns/injector/inject.test.js b/tests/browser/packages/support/concerns/injector/inject.test.js new file mode 100644 index 00000000..1dd340a1 --- /dev/null +++ b/tests/browser/packages/support/concerns/injector/inject.test.js @@ -0,0 +1,57 @@ +import makeConcernsInjector from "../helpers/makeConcernsInjector"; +import { AbstractConcern } from "@aedart/support/concerns"; + +describe('@aedart/support/concerns', () => { + describe('ConcernsInjector', () => { + describe('inject()', () => { + + it('can inject concerns into target class', () => { + + class ConcernA extends AbstractConcern { + foo() { + return 'foo'; + } + } + class ConcernB extends AbstractConcern { + bar() { + return 'bar' + } + } + class ConcernC extends AbstractConcern { + get message() { + return 'Hi...' + } + } + + /** + * @property {() => string} foo + * @property {() => string} bar + * @property {string} greeting + */ + class A {} + + const injector = makeConcernsInjector(A); + + // --------------------------------------------------------------------------- // + + const target = injector.inject( + ConcernA, + ConcernB, + { concern: ConcernC, aliases: { 'message': 'greeting' } } + ); + + // --------------------------------------------------------------------------- // + + const instance = new A(); + + expect(instance.foo()) + .toBe('foo'); + expect(instance.bar()) + .toBe('bar'); + expect(instance.greeting) + .toBe('Hi...'); + }); + + }); + }); +}); \ No newline at end of file From 93b1d22668a8c2cd3f7c2b6a7840e0c77388acac Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 11:18:11 +0100 Subject: [PATCH 365/424] Improve internal types --- packages/support/src/concerns/ConcernsInjector.ts | 3 +-- packages/support/src/concerns/ConfigurationFactory.ts | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 5b4be51b..c4667e5c 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -514,8 +514,7 @@ export default class ConcernsInjector implements Injector const properties: PropertyKey[] = Reflect.ownKeys(aliases); for (const key of properties) { - // @ts-expect-error Alias is obtained correctly here... - const alias: Alias = aliases[key] as Alias; + const alias: Alias = aliases[key as keyof typeof aliases] as Alias; // Ensure that alias does not conflict with previous applied aliases. this.assertAliasDoesNotConflict( diff --git a/packages/support/src/concerns/ConfigurationFactory.ts b/packages/support/src/concerns/ConfigurationFactory.ts index 47855ca3..caa526b2 100644 --- a/packages/support/src/concerns/ConfigurationFactory.ts +++ b/packages/support/src/concerns/ConfigurationFactory.ts @@ -124,11 +124,10 @@ export default class ConfigurationFactory implements Factory */ protected removeUnsafeKeys(configuration: Configuration): Configuration { - const keys: PropertyKey[] = Reflect.ownKeys(configuration.aliases as object); + const keys: PropertyKey[] = Reflect.ownKeys(configuration.aliases as Aliases); for (const key of keys) { if (this.isUnsafe(key)) { - // @ts-expect-error Property Key does exist at this point. - delete configuration.aliases[key]; + delete (configuration.aliases as Aliases)[key as keyof typeof configuration.aliases]; } } From cee6197e29a8cf77db7922c0985405d5746da5b3 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 11:26:49 +0100 Subject: [PATCH 366/424] Improve JSDoc and method signature --- packages/support/src/concerns/use.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/support/src/concerns/use.ts b/packages/support/src/concerns/use.ts index 363f2ddb..ff58e10b 100644 --- a/packages/support/src/concerns/use.ts +++ b/packages/support/src/concerns/use.ts @@ -1,36 +1,38 @@ -import { - Concern, +import type { + ConcernConstructor, Configuration } from "@aedart/contracts/support/concerns"; -import type { Constructor } from "@aedart/contracts"; import ConcernsInjector from "./ConcernsInjector"; /** - * Injects the given concern classes into target class + * Injects the concern classes into the target class * - * **Note**: _Method is intended to be used as a decorator!_ + * **Note**: _Method is intended to be used as a class decorator!_ * * **Example**: * ``` * @use( * MyConcernA, * MyConcernB, - * MyConcernC, + * { concern: MyConcernC, aliases: { 'foo': 'bar' } }, * ) * class MyClass {} * ``` * * @see Injector + * @see ConcernConstructor + * @see Configuration + * @see UsesConcerns * - * @template C = {@link Concern} + * @template T = object * - * @param {...Constructor | Configuration} concerns + * @param {...Constructor | Configuration<} concerns * - * @returns {(target: object) => UsesConcerns} + * @returns {(target: T) => UsesConcerns} * * @throws {InjectionException} */ -export function use(...concerns: (Constructor|Configuration)[]) +export function use(...concerns: (ConcernConstructor|Configuration)[]) { return (target: object) => { return (new ConcernsInjector(target)).inject(...concerns); From e1506bf3e035ea1a802d69c53c7b4a9f362b5c9a Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 12:44:07 +0100 Subject: [PATCH 367/424] Add test for use() class decorator Most tests have already been completed for the Concerns Injector. Here, the test just ensure that the class decorator works. --- .../packages/support/concerns/use.test.js | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/browser/packages/support/concerns/use.test.js diff --git a/tests/browser/packages/support/concerns/use.test.js b/tests/browser/packages/support/concerns/use.test.js new file mode 100644 index 00000000..8249dacc --- /dev/null +++ b/tests/browser/packages/support/concerns/use.test.js @@ -0,0 +1,51 @@ +import { AbstractConcern, use } from "@aedart/support/concerns"; + +describe('@aedart/support/concerns', () => { + describe('use()', () => { + + it('can inject concerns into target via @use() class decorator new instance', () => { + class ConcernA extends AbstractConcern { + ping() { + return 'pong'; + } + } + class ConcernB extends AbstractConcern { + foo() { + return 'bar'; + } + } + class ConcernC extends AbstractConcern { + get hi() { + return 'Hi...' + } + } + + /** @type {Configuration} */ + const config = { concern: ConcernC, aliases: { 'hi': 'message' } }; + + /** + * @property {() => string} ping + * @property {() => string} foo + * @property {string} message + */ + @use( + ConcernA, + ConcernB, + config + ) + class MyService {} + + // --------------------------------------------------------------------------- // + + const instance = new MyService(); + + expect(instance.ping()) + .toBe('pong'); + expect(instance.foo()) + .toBe('bar'); + expect(instance.message) + .toBe('Hi...'); + }); + + }); +}); \ No newline at end of file From 47b9d0f10e08627714237d66c5b27b3f7ef4bd9c Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 12:56:01 +0100 Subject: [PATCH 368/424] Add isConcernsOwner() util function --- packages/support/src/concerns/index.ts | 1 + .../support/src/concerns/isConcernsOwner.ts | 14 +++++++ .../support/concerns/isConcernsOwner.test.js | 37 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 packages/support/src/concerns/isConcernsOwner.ts create mode 100644 tests/browser/packages/support/concerns/isConcernsOwner.test.js diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 06a4b02e..1e5e747d 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -19,5 +19,6 @@ export { export * from './exceptions'; export * from './isConcernConfiguration'; export * from './isConcernConstructor'; +export * from './isConcernsOwner'; export * from './isUnsafeKey'; export * from './use'; \ No newline at end of file diff --git a/packages/support/src/concerns/isConcernsOwner.ts b/packages/support/src/concerns/isConcernsOwner.ts new file mode 100644 index 00000000..8fb4f6ba --- /dev/null +++ b/packages/support/src/concerns/isConcernsOwner.ts @@ -0,0 +1,14 @@ +import { CONCERNS } from "@aedart/contracts/support/concerns"; +import { isset } from "@aedart/support/misc"; + +/** + * Determine if object is of the type [Concerns Owner]{@link import('@aedart/contracts/support/concerns').Owner} + * + * @param {object} instance + * + * @return {boolean} + */ +export function isConcernsOwner(instance: object): boolean +{ + return isset(instance) && Reflect.has(instance, CONCERNS); +} \ No newline at end of file diff --git a/tests/browser/packages/support/concerns/isConcernsOwner.test.js b/tests/browser/packages/support/concerns/isConcernsOwner.test.js new file mode 100644 index 00000000..f06bc36e --- /dev/null +++ b/tests/browser/packages/support/concerns/isConcernsOwner.test.js @@ -0,0 +1,37 @@ +import { CONCERNS } from "@aedart/contracts/support/concerns"; +import { isConcernsOwner } from "@aedart/support/concerns"; + +describe('@aedart/support/concerns', () => { + describe('isConcernsOwner()', () => { + + it('can determine if target instance is a concern owner', () => { + + class A {} + + class B { + get [CONCERNS]() {} + } + + class C extends B {} + + // ------------------------------------------------------------------------------------ // + + const data = [ + { value: null, expected: false, name: 'Null' }, + { value: [], expected: false, name: 'Array' }, + { value: {}, expected: false, name: 'Object (empty)' }, + { value: A, expected: false, name: 'Class A (empty)' }, + + { value: new B(), expected: true, name: 'Class B instance' }, + { value: new C(), expected: true, name: 'Class C instance (inherits Class B)' }, + ]; + + for (const entry of data) { + expect(isConcernsOwner(entry.value)) + .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) + .toBe(entry.expected); + } + }); + + }); +}); \ No newline at end of file From d15af214694eef4250f823cb21163724309b6a8b Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 13:17:31 +0100 Subject: [PATCH 369/424] Add getConcernsContainer() util function --- .../src/concerns/getConcernsContainer.ts | 26 ++++++++++++++ packages/support/src/concerns/index.ts | 1 + .../concerns/getConcernsContainer.test.js | 35 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 packages/support/src/concerns/getConcernsContainer.ts create mode 100644 tests/browser/packages/support/concerns/getConcernsContainer.test.js diff --git a/packages/support/src/concerns/getConcernsContainer.ts b/packages/support/src/concerns/getConcernsContainer.ts new file mode 100644 index 00000000..d2f2f34d --- /dev/null +++ b/packages/support/src/concerns/getConcernsContainer.ts @@ -0,0 +1,26 @@ +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { Container, Owner } from "@aedart/contracts/support/concerns"; +import { CONCERNS } from "@aedart/contracts/support/concerns"; +import { isConcernsOwner } from "./isConcernsOwner"; +import { getNameOrDesc } from "@aedart/support/reflections"; + +/** + * Returns [owner's]{@link Owner} [concerns container]{@link Container} + * + * @see isConcernsOwner + * + * @param {object|Owner} instance + * + * @return {Container} + * + * @throws {TypeError} If `instance` is not a [concerns owner]{@link Owner} + */ +export function getConcernsContainer(instance: object|Owner): Container +{ + if (!isConcernsOwner(instance)) { + const msg: string = `${getNameOrDesc(instance as ConstructorOrAbstractConstructor)} is not a concerns owner`; + throw new TypeError(msg, { cause: { instance: instance } }); + } + + return (instance as Owner)[CONCERNS]; +} \ No newline at end of file diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 1e5e747d..69244d86 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -17,6 +17,7 @@ export { }; export * from './exceptions'; +export * from './getConcernsContainer'; export * from './isConcernConfiguration'; export * from './isConcernConstructor'; export * from './isConcernsOwner'; diff --git a/tests/browser/packages/support/concerns/getConcernsContainer.test.js b/tests/browser/packages/support/concerns/getConcernsContainer.test.js new file mode 100644 index 00000000..759fff91 --- /dev/null +++ b/tests/browser/packages/support/concerns/getConcernsContainer.test.js @@ -0,0 +1,35 @@ + +import { AbstractConcern, ConcernsContainer, getConcernsContainer, use } from "@aedart/support/concerns"; + +describe('@aedart/support/concerns', () => { + describe('getConcernsContainer()', () => { + + it('fails of target instance is not a concerns owner', () => { + class A {} + + // ------------------------------------------------------------------------------- // + + const callback = () => { + getConcernsContainer(new A()); + } + + expect(callback) + .toThrowError(TypeError) + }); + + it('returns concerns container', () => { + + class MyConcern extends AbstractConcern {} + + @use(MyConcern) + class A {} + + // ------------------------------------------------------------------------------- // + + const result = getConcernsContainer(new A()); + + expect(result) + .toBeInstanceOf(ConcernsContainer); + }); + }); +}); \ No newline at end of file From f7a40bcdfd6b2bed3107eedf51e5b841b07d33b0 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 13:51:59 +0100 Subject: [PATCH 370/424] Add usesConcerns() util function --- packages/support/src/concerns/index.ts | 3 +- packages/support/src/concerns/usesConcerns.ts | 27 ++++++++++ .../support/concerns/usesConcerns.test.js | 52 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 packages/support/src/concerns/usesConcerns.ts create mode 100644 tests/browser/packages/support/concerns/usesConcerns.test.js diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 69244d86..5218aa03 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -22,4 +22,5 @@ export * from './isConcernConfiguration'; export * from './isConcernConstructor'; export * from './isConcernsOwner'; export * from './isUnsafeKey'; -export * from './use'; \ No newline at end of file +export * from './use'; +export * from './usesConcerns'; \ No newline at end of file diff --git a/packages/support/src/concerns/usesConcerns.ts b/packages/support/src/concerns/usesConcerns.ts new file mode 100644 index 00000000..ea2b1825 --- /dev/null +++ b/packages/support/src/concerns/usesConcerns.ts @@ -0,0 +1,27 @@ +import type { ConcernConstructor, Container, Owner } from "@aedart/contracts/support/concerns"; +import { isConcernsOwner } from "./isConcernsOwner"; +import { getConcernsContainer } from "./getConcernsContainer"; + +/** + * Determine if [concerns owner]{@link Owner} uses the given concerns + * + * @param {object|Owner} instance + * @param {...ConcernConstructor[]} concerns + * + * @return {boolean} `true` if owner uses all given concerns, `false` otherwise. + */ +export function usesConcerns(instance: object|Owner, ...concerns: ConcernConstructor[]): boolean +{ + if (!isConcernsOwner(instance) || concerns.length == 0) { + return false; + } + + const container: Container = getConcernsContainer(instance); + for (const concern of concerns){ + if (!container.has(concern)) { + return false; + } + } + + return true; +} \ No newline at end of file diff --git a/tests/browser/packages/support/concerns/usesConcerns.test.js b/tests/browser/packages/support/concerns/usesConcerns.test.js new file mode 100644 index 00000000..34bc0459 --- /dev/null +++ b/tests/browser/packages/support/concerns/usesConcerns.test.js @@ -0,0 +1,52 @@ +import {usesConcerns, AbstractConcern, use} from "@aedart/support/concerns"; + +describe('@aedart/support/concerns', () => { + describe('usesConcerns()', () => { + + it('can determine if target instance uses concerns', () => { + + class ConcernA extends AbstractConcern {} + class ConcernB extends AbstractConcern {} + class ConcernC extends AbstractConcern {} + + class ConcernD extends AbstractConcern {} + + class A {} + + @use( + ConcernA, + ConcernB + ) + class B {} + + @use(ConcernC) + class C extends B {} + + // ------------------------------------------------------------------------------------ // + + const data = [ + { instance: null, concerns: [], expected: false, name: 'Null' }, + { instance: [], concerns: [], expected: false, name: 'Array' }, + { instance: {}, concerns: [], expected: false, name: 'Object (empty)' }, + { instance: A, concerns: [], expected: false, name: 'Class A (empty)' }, + { instance: new A(), concerns: [ ConcernA ], expected: false, name: 'A' }, + { instance: new C(), concerns: [], expected: false, name: 'C, concerns: (empty)' }, + + { instance: new C(), concerns: [ ConcernA ], expected: true, name: 'C, concerns: a' }, + { instance: new C(), concerns: [ ConcernB ], expected: true, name: 'C, concerns: b' }, + { instance: new C(), concerns: [ ConcernC ], expected: true, name: 'C, concerns: c' }, + { instance: new C(), concerns: [ ConcernA, ConcernB ], expected: true, name: 'C, concerns: a, b' }, + { instance: new C(), concerns: [ ConcernA, ConcernB, ConcernC ], expected: true, name: 'C, concerns: a, b, c' }, + + { instance: new C(), concerns: [ ConcernA, ConcernB, ConcernC, ConcernD ], expected: false, name: 'C instance, concerns: a, b, c and d (not used)' }, + ]; + + for (const entry of data) { + expect(usesConcerns(entry.instance, ...entry.concerns)) + .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) + .toBe(entry.expected); + } + }); + + }); +}); \ No newline at end of file From 4585e2809ce08ecf127198d068496cad1a7a9ca1 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 14:02:05 +0100 Subject: [PATCH 371/424] Add getContainer() util, which does not perform any type check --- .../src/concerns/getConcernsContainer.ts | 5 +++-- packages/support/src/concerns/getContainer.ts | 18 ++++++++++++++++++ packages/support/src/concerns/index.ts | 1 + packages/support/src/concerns/usesConcerns.ts | 4 ++-- 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 packages/support/src/concerns/getContainer.ts diff --git a/packages/support/src/concerns/getConcernsContainer.ts b/packages/support/src/concerns/getConcernsContainer.ts index d2f2f34d..15d03d12 100644 --- a/packages/support/src/concerns/getConcernsContainer.ts +++ b/packages/support/src/concerns/getConcernsContainer.ts @@ -1,13 +1,14 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import type { Container, Owner } from "@aedart/contracts/support/concerns"; -import { CONCERNS } from "@aedart/contracts/support/concerns"; import { isConcernsOwner } from "./isConcernsOwner"; import { getNameOrDesc } from "@aedart/support/reflections"; +import { getContainer } from "./getContainer"; /** * Returns [owner's]{@link Owner} [concerns container]{@link Container} * * @see isConcernsOwner + * @see getContainer * * @param {object|Owner} instance * @@ -22,5 +23,5 @@ export function getConcernsContainer(instance: object|Owner): Container throw new TypeError(msg, { cause: { instance: instance } }); } - return (instance as Owner)[CONCERNS]; + return getContainer(instance as Owner); } \ No newline at end of file diff --git a/packages/support/src/concerns/getContainer.ts b/packages/support/src/concerns/getContainer.ts new file mode 100644 index 00000000..961d55a1 --- /dev/null +++ b/packages/support/src/concerns/getContainer.ts @@ -0,0 +1,18 @@ +import type { Container, Owner } from "@aedart/contracts/support/concerns"; +import { CONCERNS } from "@aedart/contracts/support/concerns"; + +/** + * Returns [owner's]{@link Owner} [concerns container]{@link Container} + * + * **Caution**: _Method expects that given `owner` is type {@link Owner}!_ + * + * @see getConcernsContainer + * + * @param {Owner} owner + * + * @return {Container} + */ +export function getContainer(owner: Owner): Container +{ + return owner[CONCERNS]; +} \ No newline at end of file diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 5218aa03..65d0858b 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -18,6 +18,7 @@ export { export * from './exceptions'; export * from './getConcernsContainer'; +export * from './getContainer'; export * from './isConcernConfiguration'; export * from './isConcernConstructor'; export * from './isConcernsOwner'; diff --git a/packages/support/src/concerns/usesConcerns.ts b/packages/support/src/concerns/usesConcerns.ts index ea2b1825..e0154310 100644 --- a/packages/support/src/concerns/usesConcerns.ts +++ b/packages/support/src/concerns/usesConcerns.ts @@ -1,6 +1,6 @@ import type { ConcernConstructor, Container, Owner } from "@aedart/contracts/support/concerns"; import { isConcernsOwner } from "./isConcernsOwner"; -import { getConcernsContainer } from "./getConcernsContainer"; +import { getContainer } from "./getContainer"; /** * Determine if [concerns owner]{@link Owner} uses the given concerns @@ -16,7 +16,7 @@ export function usesConcerns(instance: object|Owner, ...concerns: ConcernConstru return false; } - const container: Container = getConcernsContainer(instance); + const container: Container = getContainer(instance as Owner); for (const concern of concerns){ if (!container.has(concern)) { return false; From 1aa6c0b33ea833352debe739a1f43c097dd898cf Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 14:08:32 +0100 Subject: [PATCH 372/424] Add assertIsConcernsOwner() util function --- .../src/concerns/assertIsConcernsOwner.ts | 20 +++++++++++++++++++ .../src/concerns/getConcernsContainer.ts | 11 +++------- packages/support/src/concerns/index.ts | 1 + 3 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 packages/support/src/concerns/assertIsConcernsOwner.ts diff --git a/packages/support/src/concerns/assertIsConcernsOwner.ts b/packages/support/src/concerns/assertIsConcernsOwner.ts new file mode 100644 index 00000000..73aff60d --- /dev/null +++ b/packages/support/src/concerns/assertIsConcernsOwner.ts @@ -0,0 +1,20 @@ +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { getNameOrDesc } from "@aedart/support/reflections"; +import { isConcernsOwner } from "./isConcernsOwner"; + +/** + * Assert that given instance is of the type [Concerns Owner]{@link import('@aedart/contracts/support/concerns').Owner} + * + * @see isConcernsOwner + * + * @param {object} instance + * + * @throws {TypeError} If `instance` is not of the type [Concerns Owner]{@link import('@aedart/contracts/support/concerns').Owner} + */ +export function assertIsConcernsOwner(instance: object): void +{ + if (!isConcernsOwner(instance)) { + const msg: string = `${getNameOrDesc(instance as ConstructorOrAbstractConstructor)} is not a concerns owner`; + throw new TypeError(msg, { cause: { instance: instance } }); + } +} \ No newline at end of file diff --git a/packages/support/src/concerns/getConcernsContainer.ts b/packages/support/src/concerns/getConcernsContainer.ts index 15d03d12..30db723c 100644 --- a/packages/support/src/concerns/getConcernsContainer.ts +++ b/packages/support/src/concerns/getConcernsContainer.ts @@ -1,13 +1,11 @@ -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import type { Container, Owner } from "@aedart/contracts/support/concerns"; -import { isConcernsOwner } from "./isConcernsOwner"; -import { getNameOrDesc } from "@aedart/support/reflections"; +import { assertIsConcernsOwner } from "./assertIsConcernsOwner"; import { getContainer } from "./getContainer"; /** * Returns [owner's]{@link Owner} [concerns container]{@link Container} * - * @see isConcernsOwner + * @see assertIsConcernsOwner * @see getContainer * * @param {object|Owner} instance @@ -18,10 +16,7 @@ import { getContainer } from "./getContainer"; */ export function getConcernsContainer(instance: object|Owner): Container { - if (!isConcernsOwner(instance)) { - const msg: string = `${getNameOrDesc(instance as ConstructorOrAbstractConstructor)} is not a concerns owner`; - throw new TypeError(msg, { cause: { instance: instance } }); - } + assertIsConcernsOwner(instance); return getContainer(instance as Owner); } \ No newline at end of file diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 65d0858b..bdd83a90 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -17,6 +17,7 @@ export { }; export * from './exceptions'; +export * from './assertIsConcernsOwner'; export * from './getConcernsContainer'; export * from './getContainer'; export * from './isConcernConfiguration'; From 1545f977738347246b936786fc4b930fa293fe55 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 14:19:04 +0100 Subject: [PATCH 373/424] Add ALIASES symbol as unsafe property key --- packages/support/src/concerns/isUnsafeKey.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/support/src/concerns/isUnsafeKey.ts b/packages/support/src/concerns/isUnsafeKey.ts index e7dd12fc..fcaee35b 100644 --- a/packages/support/src/concerns/isUnsafeKey.ts +++ b/packages/support/src/concerns/isUnsafeKey.ts @@ -1,5 +1,5 @@ import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; -import { CONCERN_CLASSES, CONCERNS, PROVIDES } from "@aedart/contracts/support/concerns"; +import { CONCERN_CLASSES, CONCERNS, PROVIDES, ALIASES } from "@aedart/contracts/support/concerns"; /** * List of property keys that are considered "unsafe" to alias (proxy to) @@ -29,6 +29,7 @@ export const UNSAFE_PROPERTY_KEYS = [ // from being aliased. CONCERN_CLASSES, CONCERNS, + ALIASES, ]; /** From 341b82c844308bc424e1357ad93eb98e1209631e Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 14:30:49 +0100 Subject: [PATCH 374/424] Add boot- and boot-all concerns util functions --- .../support/src/concerns/bootAllConcerns.ts | 18 ++++ packages/support/src/concerns/bootConcerns.ts | 22 +++++ packages/support/src/concerns/index.ts | 2 + .../support/concerns/bootConcerns.test.js | 83 +++++++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 packages/support/src/concerns/bootAllConcerns.ts create mode 100644 packages/support/src/concerns/bootConcerns.ts create mode 100644 tests/browser/packages/support/concerns/bootConcerns.test.js diff --git a/packages/support/src/concerns/bootAllConcerns.ts b/packages/support/src/concerns/bootAllConcerns.ts new file mode 100644 index 00000000..3538f4a5 --- /dev/null +++ b/packages/support/src/concerns/bootAllConcerns.ts @@ -0,0 +1,18 @@ +import type { Owner } from "@aedart/contracts/support/concerns"; +import { getConcernsContainer } from "@aedart/support/concerns/getConcernsContainer"; + +/** + * Boot all concerns for [owner]{@link Owner} instance + * + * @param {object|Owner} instance + * + * @return {void} + * + * @throws {TypeError} If `instance` is not of the type [Concerns Owner]{@link import('@aedart/contracts/support/concerns').Owner} + * @throws {NotRegisteredError} If a concern class is not registered in this container + * @throws {BootError} If a concern is unable to be booted, e.g. if already booted + */ +export function bootAllConcerns(instance: object|Owner): void +{ + getConcernsContainer(instance).bootAll(); +} \ No newline at end of file diff --git a/packages/support/src/concerns/bootConcerns.ts b/packages/support/src/concerns/bootConcerns.ts new file mode 100644 index 00000000..e84bd319 --- /dev/null +++ b/packages/support/src/concerns/bootConcerns.ts @@ -0,0 +1,22 @@ +import type { ConcernConstructor, Container, Owner } from "@aedart/contracts/support/concerns"; +import { getConcernsContainer } from "./getConcernsContainer"; + +/** + * Boot given concerns for [owner]{@link Owner} instance + * + * @param {object|Owner} instance + * @param {...ConcernConstructor[]} concerns + * + * @return {void} + * + * @throws {TypeError} If `instance` is not of the type [Concerns Owner]{@link import('@aedart/contracts/support/concerns').Owner} + * @throws {NotRegisteredError} If a concern class is not registered in this container + * @throws {BootError} If a concern is unable to be booted, e.g. if already booted + */ +export function bootConcerns(instance: object|Owner, ...concerns: ConcernConstructor[]): void +{ + const container: Container = getConcernsContainer(instance); + for (const concern of concerns) { + container.boot(concern); + } +} \ No newline at end of file diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index bdd83a90..816ce31b 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -18,6 +18,8 @@ export { export * from './exceptions'; export * from './assertIsConcernsOwner'; +export * from './bootAllConcerns'; +export * from './bootConcerns'; export * from './getConcernsContainer'; export * from './getContainer'; export * from './isConcernConfiguration'; diff --git a/tests/browser/packages/support/concerns/bootConcerns.test.js b/tests/browser/packages/support/concerns/bootConcerns.test.js new file mode 100644 index 00000000..16b47f89 --- /dev/null +++ b/tests/browser/packages/support/concerns/bootConcerns.test.js @@ -0,0 +1,83 @@ +import {bootConcerns, bootAllConcerns, getConcernsContainer, AbstractConcern, use} from "@aedart/support/concerns"; + +describe('@aedart/support/concerns', () => { + describe('bootConcerns()', () => { + it('can boot concerns', () => { + class ConcernA extends AbstractConcern {} + class ConcernB extends AbstractConcern {} + class ConcernC extends AbstractConcern {} + + @use( + ConcernA, + ConcernB + ) + class A {} + + @use(ConcernC) + class B extends A { + constructor() { + super(); + + bootConcerns(this, ConcernA, ConcernB); + } + } + + // ------------------------------------------------------------------------------------ // + + const instance = new B(); + const container = getConcernsContainer(instance); + + expect(container.hasBooted(ConcernA)) + .withContext('Concern A not booted') + .toBeTrue(); + expect(container.hasBooted(ConcernB)) + .withContext('Concern B not booted') + .toBeTrue(); + + expect(container.hasBooted(ConcernC)) + .withContext('Concern C SHOULD NOT have booted') + .toBeFalse(); + }); + + }); + + describe('bootAllConcerns()', () => { + it('can boot all concerns', () => { + + class ConcernA extends AbstractConcern {} + class ConcernB extends AbstractConcern {} + class ConcernC extends AbstractConcern {} + + @use( + ConcernA, + ConcernB + ) + class A {} + + @use(ConcernC) + class B extends A { + constructor() { + super(); + + bootAllConcerns(this); + } + } + + + // ------------------------------------------------------------------------------------ // + + const instance = new B(); + const container = getConcernsContainer(instance); + + expect(container.hasBooted(ConcernA)) + .withContext('Concern A not booted') + .toBeTrue(); + expect(container.hasBooted(ConcernB)) + .withContext('Concern B not booted') + .toBeTrue(); + expect(container.hasBooted(ConcernC)) + .withContext('Concern C not booted') + .toBeTrue(); + }); + }); +}); \ No newline at end of file From aab500b04bdb0742e8e91c48b325a81b7797ef97 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 14:55:03 +0100 Subject: [PATCH 375/424] Add test of concern that uses another concern This works as expected :) --- .../support/concerns/use-edge-cases.test.js | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/browser/packages/support/concerns/use-edge-cases.test.js diff --git a/tests/browser/packages/support/concerns/use-edge-cases.test.js b/tests/browser/packages/support/concerns/use-edge-cases.test.js new file mode 100644 index 00000000..4c283199 --- /dev/null +++ b/tests/browser/packages/support/concerns/use-edge-cases.test.js @@ -0,0 +1,43 @@ +import { AbstractConcern, use } from "@aedart/support/concerns"; + +describe('@aedart/support/concerns', () => { + describe('Edge Cases', () => { + + it('concern can use other concern', () => { + + class ConcernA extends AbstractConcern { + ping() { + return 'pong'; + } + } + + /** + * @property {() => string} ping + */ + @use(ConcernA) + class ConcernB extends AbstractConcern { + pong() { + return 'ping'; + } + } + + /** + * @property {() => string} ping + * @property {() => string} pong + */ + @use(ConcernB) + class Game {} + + // --------------------------------------------------------------------------- // + + const instance = new Game(); + + // Game (instance).ping() -> Concern B (instance).ping() -> Concern A (instance).ping() + + expect(instance.ping()) + .toBe('pong'); + expect(instance.pong()) + .toBe('ping'); + }); + }); +}); \ No newline at end of file From 3d04ca5adbba4d4e6afe8bc2383ab66abaaec487 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 15:13:24 +0100 Subject: [PATCH 376/424] Add another example of alias usage, for @use() --- .../packages/support/concerns/use.test.js | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/browser/packages/support/concerns/use.test.js b/tests/browser/packages/support/concerns/use.test.js index 8249dacc..7716be59 100644 --- a/tests/browser/packages/support/concerns/use.test.js +++ b/tests/browser/packages/support/concerns/use.test.js @@ -18,22 +18,38 @@ describe('@aedart/support/concerns', () => { get hi() { return 'Hi...' } + + sayHi(name) { + const hi = this.hi; + return `${hi} ${name}`; + } } /** @type {Configuration} */ - const config = { concern: ConcernC, aliases: { 'hi': 'message' } }; + const config = { + concern: ConcernC, + aliases: { + 'hi': 'message', + 'sayHi': 'write' + } + }; /** * @property {() => string} ping * @property {() => string} foo * @property {string} message + * @property {(name: string) => string} write */ @use( ConcernA, ConcernB, config ) - class MyService {} + class MyService { + sayHi(name) { + return this.write(name) + '!'; + } + } // --------------------------------------------------------------------------- // @@ -45,6 +61,8 @@ describe('@aedart/support/concerns', () => { .toBe('bar'); expect(instance.message) .toBe('Hi...'); + expect(instance.sayHi('Hans')) + .toBe('Hi... Hans!'); }); }); From 5376b6348241a8897eca86f8b2e296d8dba60a3e Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 15:18:20 +0100 Subject: [PATCH 377/424] Add test where getter and setter of same property are in two concerns --- .../support/concerns/use-edge-cases.test.js | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/browser/packages/support/concerns/use-edge-cases.test.js b/tests/browser/packages/support/concerns/use-edge-cases.test.js index 4c283199..0a3d155c 100644 --- a/tests/browser/packages/support/concerns/use-edge-cases.test.js +++ b/tests/browser/packages/support/concerns/use-edge-cases.test.js @@ -1,4 +1,4 @@ -import { AbstractConcern, use } from "@aedart/support/concerns"; +import {AbstractConcern, AliasConflictError, use} from "@aedart/support/concerns"; describe('@aedart/support/concerns', () => { describe('Edge Cases', () => { @@ -39,5 +39,27 @@ describe('@aedart/support/concerns', () => { expect(instance.pong()) .toBe('ping'); }); + + it('fails when getter and setter are declared in different concerns', () => { + + class ConcernA extends AbstractConcern { + get title() {} + } + + class ConcernB extends AbstractConcern { + set title(value) {} + } + + const callback = () => { + @use( + ConcernA, + ConcernB // Conflicts with "title" from Concern A - same property key! + ) + class A {} + } + + expect(callback) + .toThrowError(AliasConflictError); + }); }); }); \ No newline at end of file From 398ae54fe7f041a138c00982420efbb8d00e1d4e Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 15:26:17 +0100 Subject: [PATCH 378/424] Fix strange test name --- tests/browser/packages/support/concerns/use.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/packages/support/concerns/use.test.js b/tests/browser/packages/support/concerns/use.test.js index 7716be59..cd87422b 100644 --- a/tests/browser/packages/support/concerns/use.test.js +++ b/tests/browser/packages/support/concerns/use.test.js @@ -3,7 +3,7 @@ import { AbstractConcern, use } from "@aedart/support/concerns"; describe('@aedart/support/concerns', () => { describe('use()', () => { - it('can inject concerns into target via @use() class decorator new instance', () => { + it('can inject concerns into target via @use() class decorator', () => { class ConcernA extends AbstractConcern { ping() { return 'pong'; From 41a0b1cf21f4db12c1d88d17cb5dd8c9496836ab Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 27 Feb 2024 15:26:44 +0100 Subject: [PATCH 379/424] Add test that shows how fluent methods can be declared --- .../support/concerns/use-edge-cases.test.js | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/browser/packages/support/concerns/use-edge-cases.test.js b/tests/browser/packages/support/concerns/use-edge-cases.test.js index 0a3d155c..ff2a0e81 100644 --- a/tests/browser/packages/support/concerns/use-edge-cases.test.js +++ b/tests/browser/packages/support/concerns/use-edge-cases.test.js @@ -61,5 +61,37 @@ describe('@aedart/support/concerns', () => { expect(callback) .toThrowError(AliasConflictError); }); + + it('can make "fluent" methods', () => { + + class ConcernA extends AbstractConcern { + with(value) { + // ...value ignored here... + + return this.concernOwner; + } + } + + /** + * @property {(value: string) => this} with Add a value to the request... + */ + @use(ConcernA) + class Service { + request() { + return 'done'; + } + } + + const instance = new Service(); + + const result = instance + .with('a') + .with('b') + .with('c') + .request(); + + expect(result) + .toBe('done'); + }); }); }); \ No newline at end of file From 462663b2b429e3dab0a42e0ac867e6fb64a6bbca Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 27 Feb 2024 18:10:27 +0100 Subject: [PATCH 380/424] Add test of alias skipped when inherited by parent --- .../support/concerns/use-edge-cases.test.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/browser/packages/support/concerns/use-edge-cases.test.js b/tests/browser/packages/support/concerns/use-edge-cases.test.js index ff2a0e81..1b986e7d 100644 --- a/tests/browser/packages/support/concerns/use-edge-cases.test.js +++ b/tests/browser/packages/support/concerns/use-edge-cases.test.js @@ -93,5 +93,30 @@ describe('@aedart/support/concerns', () => { expect(result) .toBe('done'); }); + + it('does not alias property key inherited by parent', () => { + + class A { + driver() { + return 'xyz'; + } + } + + class ConcernsDriver extends AbstractConcern { + driver() { + return 'special'; + } + } + + @use(ConcernsDriver) // driver() is NOT aliased - method inherited from class A! + class B extends A {} + + // --------------------------------------------------------------------------- // + + const instance = new B(); + + expect(instance.driver()) + .toBe('xyz'); + }); }); }); \ No newline at end of file From be14d6a22ace0c6b5304d8a3e3288478075737a1 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 27 Feb 2024 20:07:36 +0100 Subject: [PATCH 381/424] Add JSDoc tinkering Attempted to see which possible way of documenting Concerns could work in the IDE (IntelliJ). --- .../packages/support/concerns/x-jsdoc.test.js | 286 ++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 tests/browser/packages/support/concerns/x-jsdoc.test.js diff --git a/tests/browser/packages/support/concerns/x-jsdoc.test.js b/tests/browser/packages/support/concerns/x-jsdoc.test.js new file mode 100644 index 00000000..a8462e34 --- /dev/null +++ b/tests/browser/packages/support/concerns/x-jsdoc.test.js @@ -0,0 +1,286 @@ +import {AbstractConcern, use} from "@aedart/support/concerns"; + +describe('@aedart/support/concerns', () => { + describe('JSDoc ', () => { + + // NOTE: These test are NOT produce any JSDOC - they are just for tinkering + // in the IDE (PHPStorm / IntelliJ) to see what works in plain JavaScript. + + it('@property', () => { + + // @property = documentation of static properties of a class, namespace or other object! + // @see https://jsdoc.app/tags-property + + class Armor extends AbstractConcern { + + /** + * Returns the armor level + * + * @returns {number} + */ + get armor() { + return 8; + } + } + + /** + * @property {number} armor + */ + @use(Armor) + class Hero {} + + // --------------------------------------------------------------------------- // + + const instance = new Hero(); + + // Works... but "armor" is now static on Hero, and not the instance of Hero! + // E.g. Hero.armor !!! + // Another downside is that we cannot reuse JSDoc defined in the concern class. + + expect(instance.armor) + .toBe(8); + }); + + it('@member / @var', () => { + // @var and @member can be used to document "virtual" properties + // @see https://jsdoc.app/tags-member + + class Sword extends AbstractConcern { + + /** + * Returns amount of damage + * + * @returns {number} + */ + get damage() { + return 3; + } + + /** + * Returns the sword type + * + * @name type + * @return {string} + */ + get type() { + return 'unique'; + } + } + + + @use(Sword) + class Enemy { + + /** + * Virtual property + * + * @public + * @member {number} damage Alias for {@link Sword#damage} + * @memberof Enemy.prototype + */ + + /** + * @public + * @var {string} type Alias for {@link Sword#type} + * @instance + * @memberof Enemy + */ + + /** + * Fight + * + * @return {void} + */ + fight() { + // this.damage; // works + // this.type; // works also + } + } + + // --------------------------------------------------------------------------- // + + const instance = new Enemy(); + + // Works, but can be much more cumbersome do define. + // Notice the "@name" usage in the concern. That is important so that @link can reference the element! + // Downside, concern's JSDoc is not used. It's only referenced to via a @link + + // instance.type = 321; // Type check will complain here... + + expect(instance.damage) + .toBe(3); + expect(instance.type) + .toBe('unique'); + }); + + it('@borrows', () => { + // Using @borrows in combination with @member or @var, "virtual" properties can also be documented + // @see https://jsdoc.app/tags-borrows + + /** + * @extends AbstractConcern + */ + class Spell extends AbstractConcern { + + /** + * Returns amount of damage + * + * @name damage + * @returns {number} + */ + get damage() { + return 1; + } + + /** + * Returns the spell's name + * + * @name name + * @return {string} + */ + get name() { + return 'sleep'; + } + + /** + * Cast the spell + * + * @name cast + * @returns {number} Damage done + */ + cast() { + return this.damage; + } + } + + /** + * @borrows Spell#damage as damage Spell damage + */ + @use(Spell) + class Npc { + + /** + * Alias for {@link Spell#damage} + * @var damage + * @readonly + * @instance + * @memberof Npc + */ + + /** + * @borrows Spell#name as name + * @member {string} name Alias for {@link Spell#name} + * @instance + * @memberof Npc + */ + + /** + * Alias for {@link Spell#cast} + * + * @borrows Spell#cast as cast + * @function cast + * @return {number} + * @instance + * @memberof Npc + */ + } + + /** + * @extends Npc + */ + class MageNpc extends Npc {} + + // --------------------------------------------------------------------------- // + + const instance = new MageNpc(); + + // Works... but still cumbersome. + // Downside, concern's JSDoc is not used. It's only referenced to via a @link. + // Also, the {type} MUST still be provided or no type assistance is given. + + // instance.damage = 'fish'; // Ah... somehow the property is now lost is lost. + + expect(instance.damage) + .toBe(1); + expect(instance.name) + .toBe('sleep'); + expect(instance.cast()) + .toBe(instance.damage); + }); + + it('@mixin and @mixes', () => { + // @see https://jsdoc.app/tags-mixes + + /** + * @mixin + * @extends AbstractConcern (bad here, because `concernOwner` is suggested by IDE. But @extend required for some reason!!!) + */ + class Shield extends AbstractConcern { + + /** + * Returns the armor level + * + * @returns {number} + */ + get armor() { + return 8; + } + + /** + * Throw shield towards a target + * + * @param {object} target + * + * @returns {number} Damage given to target + */ + throw(target) { + // target ignored here... + return 3; + } + } + + /** + * @mixes Shield + */ + @use({ + concern: Shield, + aliases: { + 'throw': 'fight' + } + }) + class Monster { + /** + * Alias for {@link Shield#throw} + * + * @function fight + * @param {object} target The target to throw at... + * @return {number} Damage taken by target + * @instance + * @memberof Monster + */ + + /** + * Do stuff... + */ + do() { + this.fight({}); + } + } + + // --------------------------------------------------------------------------- // + + const instance = new Monster(); + + // Works, but it seems that all props / methods are also available statically. + // JSDoc is automatically inherited, so less cumbersome. + // Downside: + // - @extends AbstractConcern MUST be stated or IDE fails to understand!? + // - Cumbersome to define alias! + + expect(instance.armor) + .toBe(8); + expect(instance.fight({})) + .toBe(3); + }); + }); +}); \ No newline at end of file From 8879be6c6761fae127e532a097bd69dd8891d14e Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 28 Feb 2024 08:21:04 +0100 Subject: [PATCH 382/424] Add internal remark regarding "fluent" design test --- tests/browser/packages/support/concerns/use-edge-cases.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/browser/packages/support/concerns/use-edge-cases.test.js b/tests/browser/packages/support/concerns/use-edge-cases.test.js index 1b986e7d..e623db57 100644 --- a/tests/browser/packages/support/concerns/use-edge-cases.test.js +++ b/tests/browser/packages/support/concerns/use-edge-cases.test.js @@ -62,6 +62,7 @@ describe('@aedart/support/concerns', () => { .toThrowError(AliasConflictError); }); + // Note: this isn't really an edge case, - its more a test of intended behaviour! it('can make "fluent" methods', () => { class ConcernA extends AbstractConcern { From 32c074ee6768db1d64e6451a8bf41c631a20b4f4 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 28 Feb 2024 10:13:56 +0100 Subject: [PATCH 383/424] Add support for shorthand configuration This allows a concern injection configuration to be provided as an array. --- .../contracts/src/support/concerns/Factory.ts | 5 +- .../src/support/concerns/Injector.ts | 5 +- .../contracts/src/support/concerns/types.ts | 13 ++++- .../support/src/concerns/ConcernsInjector.ts | 15 +++--- .../src/concerns/ConfigurationFactory.ts | 44 ++++++++++++++--- packages/support/src/concerns/index.ts | 1 + .../src/concerns/isShorthandConfiguration.ts | 20 ++++++++ packages/support/src/concerns/use.ts | 7 +-- .../concerns/ConfigurationFactory.test.js | 30 ++++++++++++ .../concerns/isShorthandConfiguration.test.js | 48 +++++++++++++++++++ .../packages/support/concerns/use.test.js | 8 ++-- 11 files changed, 172 insertions(+), 24 deletions(-) create mode 100644 packages/support/src/concerns/isShorthandConfiguration.ts create mode 100644 tests/browser/packages/support/concerns/isShorthandConfiguration.test.js diff --git a/packages/contracts/src/support/concerns/Factory.ts b/packages/contracts/src/support/concerns/Factory.ts index 0235a4b4..c6054725 100644 --- a/packages/contracts/src/support/concerns/Factory.ts +++ b/packages/contracts/src/support/concerns/Factory.ts @@ -1,5 +1,6 @@ import ConcernConstructor from "./ConcernConstructor"; import Configuration from './Configuration'; +import { ShorthandConfiguration } from "./types"; /** * Concern Configuration Factory @@ -23,11 +24,11 @@ export default interface Factory * unless `allowAliases` is set to `false`, in which case all aliases are removed._ * * @param {object} target - * @param {ConcernConstructor | Configuration} entry + * @param {ConcernConstructor | Configuration | ShorthandConfiguration} entry * * @returns {Configuration} * * @throws {InjectionException} If entry is unsupported or invalid */ - make(target: object, entry: ConcernConstructor | Configuration): Configuration; + make(target: object, entry: ConcernConstructor | Configuration | ShorthandConfiguration): Configuration; } \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/Injector.ts b/packages/contracts/src/support/concerns/Injector.ts index 40684dfa..2a0346c3 100644 --- a/packages/contracts/src/support/concerns/Injector.ts +++ b/packages/contracts/src/support/concerns/Injector.ts @@ -1,6 +1,7 @@ import ConcernConstructor from "./ConcernConstructor"; import Configuration from "./Configuration"; import UsesConcerns from "./UsesConcerns"; +import { ShorthandConfiguration } from "./types"; /** * Concerns Injector @@ -34,13 +35,13 @@ export default interface Injector * * @template T = object The target class that concern classes must be injected into * - * @param {...ConcernConstructor | Configuration} concerns List of concern classes / injection configurations + * @param {...ConcernConstructor | Configuration | ShorthandConfiguration} concerns List of concern classes / injection configurations * * @returns {UsesConcerns} The modified target class * * @throws {InjectionException} */ - inject(...concerns: (ConcernConstructor|Configuration)[]): UsesConcerns; + inject(...concerns: (ConcernConstructor|Configuration|ShorthandConfiguration)[]): UsesConcerns; /** * Defines the concern classes that must be used by the target class. diff --git a/packages/contracts/src/support/concerns/types.ts b/packages/contracts/src/support/concerns/types.ts index a5d225c4..a7411cdc 100644 --- a/packages/contracts/src/support/concerns/types.ts +++ b/packages/contracts/src/support/concerns/types.ts @@ -1,3 +1,4 @@ +import ConcernConstructor from "./ConcernConstructor"; import Concern from "./Concern"; /** @@ -13,4 +14,14 @@ export type Alias = PropertyKey; * class' prototype and acts as a proxy to the original property or method inside the * concern class instance. */ -export type Aliases = { [key in keyof T]: Alias } | { [key: PropertyKey]: Alias }; \ No newline at end of file +export type Aliases = { [key in keyof T]: Alias } | { [key: PropertyKey]: Alias }; + +/** + * Shorthand Concern Injection Configuration + * + * @see [Configuration]{@link import('@aedart/contracts/support/concerns').Configuration} + */ +export type ShorthandConfiguration = + [ ConcernConstructor ] // concern class + | [ ConcernConstructor, Aliases ] // concern class with aliases + | [ ConcernConstructor, boolean ] // concern class with "allowAliases" flag \ No newline at end of file diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index c4667e5c..399d11d7 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -1,8 +1,9 @@ -import { +import type { ConcernConstructor, Injector, UsesConcerns, Configuration, + ShorthandConfiguration, Owner, Container, DescriptorsCache, @@ -132,13 +133,13 @@ export default class ConcernsInjector implements Injector * * @template T = object The target class that concern classes must be injected into * - * @param {...ConcernConstructor | Configuration} concerns List of concern classes / injection configurations + * @param {...ConcernConstructor | Configuration | ShorthandConfiguration} concerns List of concern classes / injection configurations * * @returns {UsesConcerns} The modified target class * * @throws {InjectionException} */ - inject(...concerns: (ConcernConstructor|Configuration)[]): UsesConcerns + inject(...concerns: (ConcernConstructor|Configuration|ShorthandConfiguration)[]): UsesConcerns { const configurations: Configuration[] = this.normalise(concerns); const concernClasses: ConcernConstructor[] = configurations.map((configuration) => configuration.concern); @@ -322,13 +323,13 @@ export default class ConcernsInjector implements Injector /** * Normalises given concerns into a list of concern configurations * - * @param {(ConcernConstructor | Configuration)[]} concerns + * @param {(ConcernConstructor | Configuration | ShorthandConfiguration)[]} concerns * * @returns {Configuration[]} * * @throws {InjectionError} */ - public normalise(concerns: (ConcernConstructor|Configuration)[]): Configuration[] + public normalise(concerns: (ConcernConstructor|Configuration|ShorthandConfiguration)[]): Configuration[] { const output: Configuration[] = []; @@ -384,7 +385,7 @@ export default class ConcernsInjector implements Injector /** * Normalises the given entry into a concern configuration * - * @param {ConcernConstructor | Configuration} entry + * @param {ConcernConstructor | Configuration | ShorthandConfiguration} entry * * @returns {Configuration} * @@ -392,7 +393,7 @@ export default class ConcernsInjector implements Injector * * @protected */ - protected normaliseEntry(entry: ConcernConstructor|Configuration): Configuration + protected normaliseEntry(entry: ConcernConstructor|Configuration|ShorthandConfiguration): Configuration { return this.factory.make(this.target as object, entry); } diff --git a/packages/support/src/concerns/ConfigurationFactory.ts b/packages/support/src/concerns/ConfigurationFactory.ts index caa526b2..0d8746b0 100644 --- a/packages/support/src/concerns/ConfigurationFactory.ts +++ b/packages/support/src/concerns/ConfigurationFactory.ts @@ -3,6 +3,7 @@ import type { Aliases, ConcernConstructor, Configuration, + ShorthandConfiguration, Factory } from "@aedart/contracts/support/concerns"; import { PROVIDES } from "@aedart/contracts/support/concerns"; @@ -10,6 +11,7 @@ import { getNameOrDesc } from "@aedart/support/reflections"; import { merge } from "@aedart/support/objects"; import InjectionError from "./exceptions/InjectionError"; import { isConcernConfiguration } from "./isConcernConfiguration"; +import { isShorthandConfiguration } from "./isShorthandConfiguration"; import { isConcernConstructor } from "./isConcernConstructor"; import { isUnsafeKey } from "./isUnsafeKey"; @@ -35,18 +37,22 @@ export default class ConfigurationFactory implements Factory * unless `allowAliases` is set to `false`, in which case all aliases are removed._ * * @param {object} target - * @param {ConcernConstructor | Configuration} entry + * @param {ConcernConstructor | Configuration | ShorthandConfiguration} entry * * @returns {Configuration} * * @throws {InjectionException} If entry is unsupported or invalid */ - make(target: object, entry: ConcernConstructor | Configuration): Configuration { + make(target: object, entry: ConcernConstructor | Configuration | ShorthandConfiguration): Configuration { // A) Make new configuration when concern class is given if (isConcernConstructor(entry)) { return this.makeConfiguration(entry as ConcernConstructor); } - + + if (isShorthandConfiguration(entry)) { + entry = this.makeFromShorthand(entry as ShorthandConfiguration); + } + // B) Make new configuration and merge provided configuration into it if (isConcernConfiguration(entry)) { // C) Merge given configuration with a new one, which has default aliases populated... @@ -56,22 +62,48 @@ export default class ConfigurationFactory implements Factory this.makeConfiguration((entry as Configuration).concern), entry ) - + // Clear all aliases, if not allowed if (!configuration.allowAliases) { configuration.aliases = Object.create(null); return configuration; } - + // Otherwise, filter off evt. "unsafe" keys. return this.removeUnsafeKeys(configuration); } - + // Fail if entry is neither a concern class nor a concern configuration const reason: string = `${getNameOrDesc(entry as ConstructorOrAbstractConstructor)} must be a valid Concern class or Concern Configuration` throw new InjectionError(target as ConstructorOrAbstractConstructor, null, reason, { cause: { entry: entry } }); } + /** + * Casts the shorthand configuration to a configuration object + * + * @param {ShorthandConfiguration} config + * + * @return {Configuration} + * + * @protected + */ + protected makeFromShorthand(config: ShorthandConfiguration): Configuration + { + const aliases = (typeof config[1] == 'object') + ? config[1] + : undefined; + + const allowAliases = (typeof config[1] == 'boolean') + ? config[1] + : undefined; + + return { + concern: config[0], + aliases: aliases, + allowAliases: allowAliases + }; + } + /** * Returns a new concern configuration for the given concern class * diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 816ce31b..13f9f561 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -25,6 +25,7 @@ export * from './getContainer'; export * from './isConcernConfiguration'; export * from './isConcernConstructor'; export * from './isConcernsOwner'; +export * from './isShorthandConfiguration'; export * from './isUnsafeKey'; export * from './use'; export * from './usesConcerns'; \ No newline at end of file diff --git a/packages/support/src/concerns/isShorthandConfiguration.ts b/packages/support/src/concerns/isShorthandConfiguration.ts new file mode 100644 index 00000000..82cea2f9 --- /dev/null +++ b/packages/support/src/concerns/isShorthandConfiguration.ts @@ -0,0 +1,20 @@ +import type { ShorthandConfiguration } from "@aedart/contracts/support/concerns"; +import { isConcernConstructor } from "./isConcernConstructor"; + +/** + * Determine if target a [Shorthand Concern Configuration]{@link import('@aedart/contracts/support/concerns').ShorthandConfiguration} + * + * @param {object} target + * @param {boolean} [force=false] If `false` then cached result is returned if available. + * + * @returns {boolean} + */ +export function isShorthandConfiguration(target: object, force: boolean = false): boolean +{ + // Note: A Concern Configuration (shorthand) only requires a + // Concern Constructor as its first value. + + return Array.isArray(target) + && target.length > 0 + && isConcernConstructor((target as ShorthandConfiguration)[0], force); +} \ No newline at end of file diff --git a/packages/support/src/concerns/use.ts b/packages/support/src/concerns/use.ts index ff58e10b..f148f38a 100644 --- a/packages/support/src/concerns/use.ts +++ b/packages/support/src/concerns/use.ts @@ -1,6 +1,7 @@ import type { ConcernConstructor, - Configuration + Configuration, + ShorthandConfiguration } from "@aedart/contracts/support/concerns"; import ConcernsInjector from "./ConcernsInjector"; @@ -26,13 +27,13 @@ import ConcernsInjector from "./ConcernsInjector"; * * @template T = object * - * @param {...Constructor | Configuration<} concerns + * @param {...Constructor | Configuration | ShorthandConfiguration} concerns * * @returns {(target: T) => UsesConcerns} * * @throws {InjectionException} */ -export function use(...concerns: (ConcernConstructor|Configuration)[]) +export function use(...concerns: (ConcernConstructor|Configuration|ShorthandConfiguration)[]) { return (target: object) => { return (new ConcernsInjector(target)).inject(...concerns); diff --git a/tests/browser/packages/support/concerns/ConfigurationFactory.test.js b/tests/browser/packages/support/concerns/ConfigurationFactory.test.js index 20a10d95..557a65a8 100644 --- a/tests/browser/packages/support/concerns/ConfigurationFactory.test.js +++ b/tests/browser/packages/support/concerns/ConfigurationFactory.test.js @@ -213,5 +213,35 @@ describe('@aedart/support/concerns', () => { .toBeFalse(); } }); + + it('can make configuration from shorthand', () => { + + class MyConcern extends AbstractConcern { + foo() {} + } + + const aliases = { + 'foo': 'a', + }; + const entry = [ MyConcern, aliases ]; + + const factory = makeConfigurationFactory(); + + // ------------------------------------------------------------------ // + + const configuration = factory.make(class {}, entry); + + expect(configuration.concern) + .withContext('Incorrect aliases object in configuration') + .toEqual(MyConcern); + + expect(configuration.aliases) + .withContext('Incorrect aliases object in configuration') + .toEqual({ ...aliases }); + + expect(configuration.allowAliases) + .withContext('Incorrect allowAliases in configuration') + .toBeTrue(); + }); }); }); \ No newline at end of file diff --git a/tests/browser/packages/support/concerns/isShorthandConfiguration.test.js b/tests/browser/packages/support/concerns/isShorthandConfiguration.test.js new file mode 100644 index 00000000..733ee75b --- /dev/null +++ b/tests/browser/packages/support/concerns/isShorthandConfiguration.test.js @@ -0,0 +1,48 @@ +import { PROVIDES } from "@aedart/contracts/support/concerns"; +import { isShorthandConfiguration, AbstractConcern } from "@aedart/support/concerns"; + +describe('@aedart/support/concerns', () => { + describe('isShorthandConfiguration()', () => { + + it('can determine if target is a shorthand concern configuration', () => { + + class A {} + + class B { + get concernOwner() { + return false; + } + + static [PROVIDES]() {} + } + + class C extends B {} + + class D extends AbstractConcern {} + + class E extends D {} + + // ------------------------------------------------------------------------------------ // + + const data = [ + { value: null, expected: false, name: 'Null' }, + { value: [], expected: false, name: 'Array (empty)' }, + { value: {}, expected: false, name: 'Object (empty)' }, + { value: A, expected: false, name: 'Class A (empty)' }, + { value: [ A ], expected: false, name: 'Array with Class A (not a concern)' }, + + { value: [ B ], expected: true, name: 'Array with Class B (custom implementation of concern)' }, + { value: [ C ], expected: true, name: 'Array with Class C (inherits from custom implementation)' }, + { value: [ D ], expected: true, name: 'Array with Class D (inherits from AbstractConcern)' }, + { value: [ E ], expected: true, name: 'Array with Class E (inherits from a base that inherits from AbstractConcern)' }, + ]; + + for (const entry of data) { + expect(isShorthandConfiguration(entry.value)) + .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) + .toBe(entry.expected); + } + }); + + }); +}); \ No newline at end of file diff --git a/tests/browser/packages/support/concerns/use.test.js b/tests/browser/packages/support/concerns/use.test.js index cd87422b..0d42eed6 100644 --- a/tests/browser/packages/support/concerns/use.test.js +++ b/tests/browser/packages/support/concerns/use.test.js @@ -36,13 +36,15 @@ describe('@aedart/support/concerns', () => { /** * @property {() => string} ping - * @property {() => string} foo + * @property {() => string} bar * @property {string} message * @property {(name: string) => string} write */ @use( ConcernA, - ConcernB, + [ConcernB, { + 'foo': 'bar' + }], config ) class MyService { @@ -57,7 +59,7 @@ describe('@aedart/support/concerns', () => { expect(instance.ping()) .toBe('pong'); - expect(instance.foo()) + expect(instance.bar()) .toBe('bar'); expect(instance.message) .toBe('Hi...'); From a1298adc0655d4524d94222e4bbf3515fa48693e Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 28 Feb 2024 10:19:53 +0100 Subject: [PATCH 384/424] Fix JSDoc --- packages/contracts/src/support/concerns/Container.ts | 2 +- packages/support/src/concerns/ConcernsContainer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index 32b99d68..b10fddb8 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -43,7 +43,7 @@ export default interface Container * * @param {ConcernConstructor} concern * - * @return {Concern} The booted instance of the concern class. If concern class was + * @return {T} The booted instance of the concern class. If concern class was * previously booted, then that instance is returned. * * @throws {ConcernException} diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts index b458b202..d216cf69 100644 --- a/packages/support/src/concerns/ConcernsContainer.ts +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -131,7 +131,7 @@ export default class ConcernsContainer implements Container * * @param {ConcernConstructor} concern * - * @return {Concern} New concern instance + * @return {T} New concern instance * * @throws {NotRegisteredError} If concern class is not registered in this container * @throws {BootError} If concern is unable to be booted, e.g. if already booted From 45b1a33acb7f539842fe8f279459256257e41121 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 28 Feb 2024 10:21:59 +0100 Subject: [PATCH 385/424] FIx JSDox --- packages/contracts/src/support/concerns/Container.ts | 4 ++-- packages/support/src/concerns/ConcernsContainer.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/contracts/src/support/concerns/Container.ts b/packages/contracts/src/support/concerns/Container.ts index b10fddb8..cc5ab1f3 100644 --- a/packages/contracts/src/support/concerns/Container.ts +++ b/packages/contracts/src/support/concerns/Container.ts @@ -44,7 +44,7 @@ export default interface Container * @param {ConcernConstructor} concern * * @return {T} The booted instance of the concern class. If concern class was - * previously booted, then that instance is returned. + * previously booted, then that instance is returned. * * @throws {ConcernException} */ @@ -66,7 +66,7 @@ export default interface Container * * @param {ConcernConstructor} concern * - * @return {Concern} New concern instance + * @return {T} New concern instance * * @throws {NotRegisteredException} If concern class is not registered in this container * @throws {BootException} If concern is unable to be booted, e.g. if already booted diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts index d216cf69..e263ffe3 100644 --- a/packages/support/src/concerns/ConcernsContainer.ts +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -96,10 +96,10 @@ export default class ConcernsContainer implements Container * * @template T extends {@link Concern} * - * @param {Constructor} concern + * @param {ConcernConstructor} concern * - * @return {Concern} The booted instance of the concern class. If concern class was - * previously booted, then that instance is returned. + * @return {T} The booted instance of the concern class. If concern class was + * previously booted, then that instance is returned. * * @throws {ConcernError} */ From f2a50bf4d75448b4abd344ee167a97ea17dfbd2f Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 28 Feb 2024 15:05:12 +0100 Subject: [PATCH 386/424] Add support for registration hook methods in concern classes --- .../src/support/concerns/RegistrationAware.ts | 46 ++++++++++ .../contracts/src/support/concerns/index.ts | 26 ++++++ .../support/src/concerns/AbstractConcern.ts | 62 ++++++++++++- .../support/src/concerns/ConcernsInjector.ts | 88 +++++++++++++++++-- packages/support/src/concerns/isUnsafeKey.ts | 11 ++- .../support/concerns/injector/inject.test.js | 55 ++++++++++++ 6 files changed, 279 insertions(+), 9 deletions(-) create mode 100644 packages/contracts/src/support/concerns/RegistrationAware.ts diff --git a/packages/contracts/src/support/concerns/RegistrationAware.ts b/packages/contracts/src/support/concerns/RegistrationAware.ts new file mode 100644 index 00000000..bc3ab2a1 --- /dev/null +++ b/packages/contracts/src/support/concerns/RegistrationAware.ts @@ -0,0 +1,46 @@ +import { BEFORE, AFTER } from "./index"; +import UsesConcerns from "./UsesConcerns"; + +/** + * Registration Aware + * + * Concern class is aware of when it is being registered by a target class + * and performs pre-/post-registration logic. + */ +export default interface RegistrationAware +{ + /** + * Perform pre-registration logic. + * + * **Note**: _This hook method is intended to be invoked by an + * [Injector]{@link import('@aedart/contracts/support/concerns').Injector}, before + * a concern container is and aliases are defined in the target class._ + * + * @static + * + * @param {UsesConcerns} target Target class constructor + * + * @return {void} + * + * @throws {Error} + */ + [BEFORE](target: UsesConcerns): void; + + /** + * Perform post-registration logic. + * + * **Note**: _This hook method is intended to be invoked by an + * [Injector]{@link import('@aedart/contracts/support/concerns').Injector}, after + * this concern class has been registered in a target class and aliases have been + * defined in target's prototype._ + * + * @static + * + * @param {UsesConcerns} target Target class constructor, after concerns and aliases defined + * + * @return {void} + * + * @throws {Error} + */ + [AFTER](target: UsesConcerns): void; +} \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index e5a99a21..5e0bb318 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -23,11 +23,35 @@ export const SUPPORT_CONCERNS: unique symbol = Symbol('@aedart/contracts/support * // ...remaining not shown... * } * ``` + * + * @see ConcernConstructor * * @type {symbol} */ export const PROVIDES : unique symbol = Symbol('concern_provides'); +/** + * Symbol that can be used by a [concern class]{@link ConcernConstructor} to perform + * pre-registration logic + * + * @see RegistrationAware + * @see ConcernConstructor + * + * @type {symbol} + */ +export const BEFORE: unique symbol = Symbol('concern_before_registration'); + +/** + * Symbol that can be used by a [concern class]{@link ConcernConstructor} to perform + * post-registration logic. + * + * @see RegistrationAware + * @see ConcernConstructor + * + * @type {symbol} + */ +export const AFTER: unique symbol = Symbol('concern_after_registration'); + /** * Symbol used to define a list of the concern classes to be used by a target class. * @@ -63,6 +87,7 @@ import Container from "./Container"; import DescriptorsCache from "./DescriptorsCache"; import Factory from "./Factory"; import Injector from "./Injector"; +import RegistrationAware from "./RegistrationAware"; import Owner from "./Owner"; import Resolver from "./Resolver"; import UsesConcerns from "./UsesConcerns"; @@ -74,6 +99,7 @@ export { type DescriptorsCache, type Factory, type Injector, + type RegistrationAware, type Owner, type Resolver, type UsesConcerns diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index 607d9a46..70f2c231 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -1,5 +1,5 @@ -import type { Concern } from "@aedart/contracts/support/concerns"; -import { PROVIDES } from "@aedart/contracts/support/concerns"; +import type { Concern, UsesConcerns } from "@aedart/contracts/support/concerns"; +import { PROVIDES, BEFORE, AFTER } from "@aedart/contracts/support/concerns"; import { AbstractClassError } from "@aedart/support/exceptions"; import { classOwnKeys } from "@aedart/support/reflections"; @@ -7,7 +7,8 @@ import { classOwnKeys } from "@aedart/support/reflections"; * Abstract Concern * * @see {Concern} - * @see {ConcernConstructor} + * @see [ConcernConstructor]{@link import('@aedart/contracts/support/concerns').ConcernConstructor} + * @see [RegistrationAware]{@link import('@aedart/contracts/support/concerns').RegistrationAware} * * @implements {Concern} * @@ -74,4 +75,59 @@ export default abstract class AbstractConcern implements Concern return classOwnKeys(this, true); } + + /** + * Perform pre-registration logic. + * + * **Note**: _This hook method is intended to be invoked by an + * [Injector]{@link import('@aedart/contracts/support/concerns').Injector}, before + * any concern classes are registered in the target class._ + * + * @static + * + * @param {UsesConcerns} target Target class constructor + * + * @return {void} + * + * @throws {Error} + */ + static [BEFORE]( + target: UsesConcerns /* eslint-disable-line @typescript-eslint/no-unused-vars */ + ): void + { + // Overwrite this method to perform pre-registration logic. This can be used, amongst other things, + // to prevent your concern class from being used, e.g.: + // - If your concern class is specialised and only supports specific kinds of target classes. + // - If your concern will conflict if combined with another concern class (use target[CONCERN_CLASSES] for such). + // - ...etc + } + + /** + * Perform post-registration logic. + * + * **Note**: _This hook method is intended to be invoked by an + * [Injector]{@link import('@aedart/contracts/support/concerns').Injector}, after + * this concern class has been registered in a target class and aliases have been + * defined in target's prototype._ + * + * @static + * + * @param {UsesConcerns} target Target class constructor, after concerns and aliases defined + * + * @return {void} + * + * @throws {Error} + */ + static [AFTER]( + target: UsesConcerns /* eslint-disable-line @typescript-eslint/no-unused-vars */ + ): void + { + // Overwrite this method to perform post-registration logic. This can be used in situations + // when you need to perform additional setup or configuration on the concern class level (static), + // e.g.: + // - Prepare specialised caching mechanisms, with or for the target class. + // - Obtain other kinds static resources or meta information that must be used by the target or + // concern, that otherwise cannot be done during concern class' constructor. + // - ...etc + } } \ No newline at end of file diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 399d11d7..ea5ae9d0 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -11,17 +11,21 @@ import type { Alias, Aliases, Resolver, + RegistrationAware } from "@aedart/contracts/support/concerns"; import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import { CONCERN_CLASSES, ALIASES, - CONCERNS + CONCERNS, + BEFORE, + AFTER } from "@aedart/contracts/support/concerns"; import { getAllParentsOfClass, getNameOrDesc } from "@aedart/support/reflections"; +import { getErrorMessage } from "@aedart/support/exceptions"; import AliasConflictError from './exceptions/AliasConflictError'; import AlreadyRegisteredError from './exceptions/AlreadyRegisteredError'; import InjectionError from './exceptions/InjectionError'; @@ -29,8 +33,8 @@ import UnsafeAliasError from './exceptions/UnsafeAliasError'; import ConcernsContainer from './ConcernsContainer'; import ConfigurationFactory from "./ConfigurationFactory"; import Descriptors from "./Descriptors"; -import { isUnsafeKey } from "./isUnsafeKey"; import ProxyResolver from "./ProxyResolver"; +import { isUnsafeKey } from "./isUnsafeKey"; /** * A map of the concern owner instances and their concerns container @@ -143,17 +147,28 @@ export default class ConcernsInjector implements Injector { const configurations: Configuration[] = this.normalise(concerns); const concernClasses: ConcernConstructor[] = configurations.map((configuration) => configuration.concern); + + // Define the concerns classes in target. + let modifiedTarget: UsesConcerns = this.defineConcerns(this.target, concernClasses); - const modifiedTarget = this.defineAliases( + // Run before registration hook + this.runBeforeRegistration(this.target as UsesConcerns, modifiedTarget[CONCERN_CLASSES]); + + // Define concerns, container and aliases + modifiedTarget = this.defineAliases( this.defineContainer( - this.defineConcerns(this.target, concernClasses) + modifiedTarget ), configurations ); + + // Run after registration hook + this.runAfterRegistration(modifiedTarget as UsesConcerns, modifiedTarget[CONCERN_CLASSES]); - // Clear evt. cached descriptors... + // Clear evt. cached items. this.descriptors.clear(); + // Finally, return the modified target return modifiedTarget; } @@ -585,4 +600,67 @@ export default class ConcernsInjector implements Injector { return isUnsafeKey(key); } + + /** + * Invokes the {@link BEFORE} hook method in concern classes + * + * @param {UsesConcerns} target + * @param {ConcernConstructor[]} concerns All concern classes in target's prototype chain + * + * @protected + * + * @throws {InjectionError} + */ + protected runBeforeRegistration(target: UsesConcerns, concerns: ConcernConstructor[]): void + { + this.runRegistrationHook(target, concerns, BEFORE, 'Before'); + } + + /** + * Invokes the {@link AFTER} hook method in concern classes + * + * @param {UsesConcerns} target + * @param {ConcernConstructor[]} concerns All concern classes in target's prototype chain + * + * @protected + * + * @throws {InjectionError} + */ + protected runAfterRegistration(target: UsesConcerns, concerns: ConcernConstructor[]): void + { + this.runRegistrationHook(target, concerns, AFTER, 'After'); + } + + /** + * Run registration hook on given concern classes + * + * @param {UsesConcerns} target + * @param {ConcernConstructor} concerns + * @param {symbol} hook + * @param {string} name + * + * @protected + * + * @throws {InjectionError} + */ + protected runRegistrationHook( + target: UsesConcerns, + concerns: ConcernConstructor[], + hook: symbol, + name: string + ): void + { + for (const concern of concerns) { + if (!Reflect.has(concern, hook)) { + continue; + } + + try { + (concern as ConcernConstructor & RegistrationAware)[hook as keyof RegistrationAware](target); + } catch (e) { + const reason = getErrorMessage(e); + throw new InjectionError(target, concern, `${name} registration failure: ${reason}`, { cause: { previous: e }}); + } + } + } } \ No newline at end of file diff --git a/packages/support/src/concerns/isUnsafeKey.ts b/packages/support/src/concerns/isUnsafeKey.ts index fcaee35b..dc51b591 100644 --- a/packages/support/src/concerns/isUnsafeKey.ts +++ b/packages/support/src/concerns/isUnsafeKey.ts @@ -1,5 +1,12 @@ import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; -import { CONCERN_CLASSES, CONCERNS, PROVIDES, ALIASES } from "@aedart/contracts/support/concerns"; +import { + CONCERN_CLASSES, + CONCERNS, + PROVIDES, + ALIASES, + BEFORE, + AFTER +} from "@aedart/contracts/support/concerns"; /** * List of property keys that are considered "unsafe" to alias (proxy to) @@ -20,6 +27,8 @@ export const UNSAFE_PROPERTY_KEYS = [ // The static properties and methods (just in case...) PROVIDES, + BEFORE, + AFTER, // ----------------------------------------------------------------- // // Other properties and methods: diff --git a/tests/browser/packages/support/concerns/injector/inject.test.js b/tests/browser/packages/support/concerns/injector/inject.test.js index 1dd340a1..8abc4283 100644 --- a/tests/browser/packages/support/concerns/injector/inject.test.js +++ b/tests/browser/packages/support/concerns/injector/inject.test.js @@ -1,5 +1,6 @@ import makeConcernsInjector from "../helpers/makeConcernsInjector"; import { AbstractConcern } from "@aedart/support/concerns"; +import { BEFORE, AFTER } from "@aedart/contracts/support/concerns"; describe('@aedart/support/concerns', () => { describe('ConcernsInjector', () => { @@ -52,6 +53,60 @@ describe('@aedart/support/concerns', () => { .toBe('Hi...'); }); + it('invokes before and after registration hooks', () => { + const called = []; + + class ConcernA extends AbstractConcern + { + static [BEFORE](target) { + // console.log('before (a)', target); + called.push('before a'); + } + + static [AFTER](target) { + // console.log('after (a)', target); + called.push('after a'); + } + } + class ConcernB extends AbstractConcern + { + static [BEFORE](target) { + // console.log('before (b)', target); + called.push('before b'); + } + + static [AFTER](target) { + // console.log('after (b)', target); + called.push('after b'); + } + } + + + class A {} + class B extends A {} + + // --------------------------------------------------------------------------- // + + makeConcernsInjector(A) + .inject(ConcernA); + makeConcernsInjector(B) + .inject(ConcernB); + + expect(called) + .withContext('Incorrect order of registration hooks execution') + .toEqual([ + // class A concerns + 'before a', + 'after a', + + // class B concerns (inherited from A) + 'before a', + 'before b', + 'after a', + 'after b', + ]); + }); + }); }); }); \ No newline at end of file From dc30136fd1ab0a82e04c3a069ebd880042cf0a12 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 28 Feb 2024 15:10:27 +0100 Subject: [PATCH 387/424] Improve class description --- packages/contracts/src/support/concerns/RegistrationAware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/concerns/RegistrationAware.ts b/packages/contracts/src/support/concerns/RegistrationAware.ts index bc3ab2a1..44831d0e 100644 --- a/packages/contracts/src/support/concerns/RegistrationAware.ts +++ b/packages/contracts/src/support/concerns/RegistrationAware.ts @@ -5,7 +5,7 @@ import UsesConcerns from "./UsesConcerns"; * Registration Aware * * Concern class is aware of when it is being registered by a target class - * and performs pre-/post-registration logic. + * and is able to performs pre-/post-registration logic. */ export default interface RegistrationAware { From 2e992be174fee90340c81acb222def26ee22336f Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 28 Feb 2024 15:10:35 +0100 Subject: [PATCH 388/424] Fix types --- packages/contracts/src/support/mixins/types.ts | 4 ++-- packages/support/src/mixins/Builder.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/contracts/src/support/mixins/types.ts b/packages/contracts/src/support/mixins/types.ts index fd92fe98..01e2c100 100644 --- a/packages/contracts/src/support/mixins/types.ts +++ b/packages/contracts/src/support/mixins/types.ts @@ -14,6 +14,6 @@ import type { AbstractConstructor } from "@aedart/contracts"; * ``` */ export type MixinFunction< - SuperClass extends AbstractConstructor = object, - AbstractSubclass extends AbstractConstructor = object + SuperClass extends AbstractConstructor = AbstractConstructor, + AbstractSubclass extends AbstractConstructor = AbstractConstructor > = (superclass: SuperClass) => AbstractSubclass & SuperClass; \ No newline at end of file diff --git a/packages/support/src/mixins/Builder.ts b/packages/support/src/mixins/Builder.ts index 8696ae54..191ff401 100644 --- a/packages/support/src/mixins/Builder.ts +++ b/packages/support/src/mixins/Builder.ts @@ -1,4 +1,4 @@ -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type {AbstractConstructor, ConstructorOrAbstractConstructor} from "@aedart/contracts"; import type { MixinFunction } from "@aedart/contracts/support/mixins"; /** @@ -28,7 +28,7 @@ export default class Builder * * @param {ConstructorOrAbstractConstructor} [superclass=class {}] */ - constructor(superclass:ConstructorOrAbstractConstructor = class {}) { + constructor(superclass:ConstructorOrAbstractConstructor = class {} as ConstructorOrAbstractConstructor) { this.#superclass = superclass; } @@ -42,7 +42,7 @@ export default class Builder public with(...mixins: MixinFunction[]): ConstructorOrAbstractConstructor { return mixins.reduce(( - superclass: T, + superclass: ConstructorOrAbstractConstructor, mixin: MixinFunction ) => { // Return superclass, when mixin isn't a function. @@ -52,7 +52,7 @@ export default class Builder // Apply the mixin... return mixin(superclass); - }, this.#superclass); + }, this.#superclass as ConstructorOrAbstractConstructor) as ConstructorOrAbstractConstructor; } /** From b0012c080c819a21e365c0945580099e5dff60f8 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 28 Feb 2024 15:57:26 +0100 Subject: [PATCH 389/424] Use @mixin / @mixes to describe concerns used by Game class --- .../packages/support/concerns/use-edge-cases.test.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/browser/packages/support/concerns/use-edge-cases.test.js b/tests/browser/packages/support/concerns/use-edge-cases.test.js index e623db57..bbbc6a69 100644 --- a/tests/browser/packages/support/concerns/use-edge-cases.test.js +++ b/tests/browser/packages/support/concerns/use-edge-cases.test.js @@ -5,6 +5,10 @@ describe('@aedart/support/concerns', () => { it('concern can use other concern', () => { + /** + * @mixin + * @extends AbstractConcern + */ class ConcernA extends AbstractConcern { ping() { return 'pong'; @@ -12,7 +16,8 @@ describe('@aedart/support/concerns', () => { } /** - * @property {() => string} ping + * @mixes ConcernA + * @extends AbstractConcern */ @use(ConcernA) class ConcernB extends AbstractConcern { @@ -22,8 +27,7 @@ describe('@aedart/support/concerns', () => { } /** - * @property {() => string} ping - * @property {() => string} pong + * @mixes ConcernB */ @use(ConcernB) class Game {} From 8c45af165d910a6b28de5250ff42b74366bb4377 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 09:07:06 +0100 Subject: [PATCH 390/424] Fix style --- packages/support/src/mixins/Builder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/mixins/Builder.ts b/packages/support/src/mixins/Builder.ts index 191ff401..744fc76d 100644 --- a/packages/support/src/mixins/Builder.ts +++ b/packages/support/src/mixins/Builder.ts @@ -1,4 +1,4 @@ -import type {AbstractConstructor, ConstructorOrAbstractConstructor} from "@aedart/contracts"; +import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; import type { MixinFunction } from "@aedart/contracts/support/mixins"; /** From c739ce8772bfde71fbad001599e3001830782926 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 09:31:15 +0100 Subject: [PATCH 391/424] Rename "proxy resolver" to Alias Descriptor Factory Component's responsibility is much more clear now. --- .../{Resolver.ts => AliasDescriptorFactory.ts} | 10 +++++----- .../contracts/src/support/concerns/index.ts | 4 ++-- .../support/src/concerns/ConcernsInjector.ts | 18 +++++++++--------- .../{ProxyResolver.ts => DescriptorFactory.ts} | 10 +++++----- packages/support/src/concerns/index.ts | 4 ++-- 5 files changed, 23 insertions(+), 23 deletions(-) rename packages/contracts/src/support/concerns/{Resolver.ts => AliasDescriptorFactory.ts} (57%) rename packages/support/src/concerns/{ProxyResolver.ts => DescriptorFactory.ts} (93%) diff --git a/packages/contracts/src/support/concerns/Resolver.ts b/packages/contracts/src/support/concerns/AliasDescriptorFactory.ts similarity index 57% rename from packages/contracts/src/support/concerns/Resolver.ts rename to packages/contracts/src/support/concerns/AliasDescriptorFactory.ts index 72a099d9..8880d3fe 100644 --- a/packages/contracts/src/support/concerns/Resolver.ts +++ b/packages/contracts/src/support/concerns/AliasDescriptorFactory.ts @@ -1,18 +1,18 @@ import ConcernConstructor from './ConcernConstructor'; /** - * Proxy Descriptor Resolver + * Alias Descriptor Factory */ -export default interface Resolver +export default interface AliasDescriptorFactory { /** - * Returns a property descriptor to be used for an "alias" property or method in a target class + * Makes a property descriptor to be used for an "alias" (proxy) property or method * * @param {PropertyKey} key The property key in `source` concern * @param {ConcernConstructor} source The concern that holds the property key - * @param {PropertyDescriptor} keyDescriptor Descriptor of `key` in `source` + * @param {PropertyDescriptor} keyDescriptor Descriptor of `key` in `source` concern * * @returns {PropertyDescriptor} Descriptor to be used for defining alias in a target class */ - resolve(key: PropertyKey, source: ConcernConstructor, keyDescriptor: PropertyDescriptor): PropertyDescriptor; + make(key: PropertyKey, source: ConcernConstructor, keyDescriptor: PropertyDescriptor): PropertyDescriptor; } \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 5e0bb318..31d468f2 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -89,7 +89,7 @@ import Factory from "./Factory"; import Injector from "./Injector"; import RegistrationAware from "./RegistrationAware"; import Owner from "./Owner"; -import Resolver from "./Resolver"; +import AliasDescriptorFactory from "./AliasDescriptorFactory"; import UsesConcerns from "./UsesConcerns"; export { type Concern, @@ -101,7 +101,7 @@ export { type Injector, type RegistrationAware, type Owner, - type Resolver, + type AliasDescriptorFactory, type UsesConcerns } diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index ea5ae9d0..7c87cb95 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -10,7 +10,7 @@ import type { Factory, Alias, Aliases, - Resolver, + AliasDescriptorFactory, RegistrationAware } from "@aedart/contracts/support/concerns"; import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; @@ -33,7 +33,7 @@ import UnsafeAliasError from './exceptions/UnsafeAliasError'; import ConcernsContainer from './ConcernsContainer'; import ConfigurationFactory from "./ConfigurationFactory"; import Descriptors from "./Descriptors"; -import ProxyResolver from "./ProxyResolver"; +import DescriptorFactory from "./DescriptorFactory"; import { isUnsafeKey } from "./isUnsafeKey"; /** @@ -81,13 +81,13 @@ export default class ConcernsInjector implements Injector protected descriptors: DescriptorsCache; /** - * Proxy Descriptor Resolver + * Alias Descriptor Factory * - * @type {Resolver} + * @type {AliasDescriptorFactory} * * @protected */ - protected proxyResolver: Resolver; + protected descriptorFactory: AliasDescriptorFactory; /** * Create a new Concerns Injector instance @@ -96,19 +96,19 @@ export default class ConcernsInjector implements Injector * * @param {T} target The target class that concerns must be injected into * @param {Factory} [factory] - * @param {Resolver} [resolver] + * @param {AliasDescriptorFactory} [descriptorFactory] * @param {DescriptorsCache} [descriptors] */ public constructor( target: T, factory?: Factory, - resolver?: Resolver, + descriptorFactory?: AliasDescriptorFactory, descriptors?: DescriptorsCache ) { this.#target = target; this.factory = factory || new ConfigurationFactory(); - this.proxyResolver = resolver || new ProxyResolver(); + this.descriptorFactory = descriptorFactory || new DescriptorFactory(); this.descriptors = descriptors || new Descriptors(); } @@ -330,7 +330,7 @@ export default class ConcernsInjector implements Injector } // Define the proxy property or method, using the concern's property descriptor to determine what must be defined. - const proxy = this.proxyResolver.resolve(key, source, concernDescriptors[key]); + const proxy = this.descriptorFactory.make(key, source, concernDescriptors[key]); return this.definePropertyInTarget(target.prototype, alias, proxy) !== undefined; } diff --git a/packages/support/src/concerns/ProxyResolver.ts b/packages/support/src/concerns/DescriptorFactory.ts similarity index 93% rename from packages/support/src/concerns/ProxyResolver.ts rename to packages/support/src/concerns/DescriptorFactory.ts index e8107033..7b375bf0 100644 --- a/packages/support/src/concerns/ProxyResolver.ts +++ b/packages/support/src/concerns/DescriptorFactory.ts @@ -1,16 +1,16 @@ import type { - Resolver, + AliasDescriptorFactory, ConcernConstructor, Owner } from "@aedart/contracts/support/concerns"; import { CONCERNS } from "@aedart/contracts/support/concerns"; /** - * Proxy Descriptor Resolver + * Alias Descriptor Factory * - * @see Resolver + * @see AliasDescriptorFactory */ -export default class ProxyResolver implements Resolver +export default class DescriptorFactory implements AliasDescriptorFactory { /** * Returns a property descriptor to be used for an "alias" property or method in a target class @@ -21,7 +21,7 @@ export default class ProxyResolver implements Resolver * * @returns {PropertyDescriptor} Descriptor to be used for defining alias in a target class */ - resolve(key: PropertyKey, source: ConcernConstructor, keyDescriptor: PropertyDescriptor): PropertyDescriptor + make(key: PropertyKey, source: ConcernConstructor, keyDescriptor: PropertyDescriptor): PropertyDescriptor { const proxy: PropertyDescriptor = Object.assign(Object.create(null), { configurable: keyDescriptor.configurable, diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 13f9f561..9302695b 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -4,7 +4,7 @@ import ConcernsContainer from "./ConcernsContainer"; import ConcernsInjector from "./ConcernsInjector"; import ConfigurationFactory from "./ConfigurationFactory"; import Descriptors from "./Descriptors"; -import ProxyResolver from "./ProxyResolver"; +import DescriptorFactory from "./DescriptorFactory"; export { AbstractConcern, @@ -13,7 +13,7 @@ export { ConcernsInjector, ConfigurationFactory, Descriptors, - ProxyResolver + DescriptorFactory }; export * from './exceptions'; From af1415d00801d8eb8500f91a1630ab5f69fe1ba0 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 09:40:40 +0100 Subject: [PATCH 392/424] Rename Descriptors Cache to Descriptors Repository --- ...ptorsCache.ts => DescriptorsRepository.ts} | 34 ++++----- .../contracts/src/support/concerns/index.ts | 4 +- .../support/src/concerns/ConcernsInjector.ts | 26 +++---- .../{Descriptors.ts => Repository.ts} | 70 +++++++++---------- packages/support/src/concerns/index.ts | 4 +- 5 files changed, 69 insertions(+), 69 deletions(-) rename packages/contracts/src/support/concerns/{DescriptorsCache.ts => DescriptorsRepository.ts} (97%) rename packages/support/src/concerns/{Descriptors.ts => Repository.ts} (86%) diff --git a/packages/contracts/src/support/concerns/DescriptorsCache.ts b/packages/contracts/src/support/concerns/DescriptorsRepository.ts similarity index 97% rename from packages/contracts/src/support/concerns/DescriptorsCache.ts rename to packages/contracts/src/support/concerns/DescriptorsRepository.ts index 307734d1..f3ac87e3 100644 --- a/packages/contracts/src/support/concerns/DescriptorsCache.ts +++ b/packages/contracts/src/support/concerns/DescriptorsRepository.ts @@ -3,12 +3,27 @@ import ConcernConstructor from './ConcernConstructor'; import UsesConcerns from './UsesConcerns'; /** - * Descriptors Cache + * Descriptors Repository * * Utility for obtaining property descriptors for a target class or concern. */ -export default interface DescriptorsCache +export default interface DescriptorsRepository { + /** + * Returns property descriptors for given target class (recursively) + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {boolean} [force=false] If `true` then method will not return evt. cached descriptors. + * @param {boolean} [cache=false] Caches the descriptors if `true`. + * + * @returns {Record} + */ + get( + target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, + force?: boolean, + cache?: boolean + ): Record; + /** * Caches property descriptors for target during the execution of callback. * @@ -34,21 +49,6 @@ export default interface DescriptorsCache */ remember(target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, force?: boolean): Record; - /** - * Returns property descriptors for given target class (recursively) - * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class - * @param {boolean} [force=false] If `true` then method will not return evt. cached descriptors. - * @param {boolean} [cache=false] Caches the descriptors if `true`. - * - * @returns {Record} - */ - get( - target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, - force?: boolean, - cache?: boolean - ): Record; - /** * Deletes cached descriptors for target * diff --git a/packages/contracts/src/support/concerns/index.ts b/packages/contracts/src/support/concerns/index.ts index 31d468f2..d045c23c 100644 --- a/packages/contracts/src/support/concerns/index.ts +++ b/packages/contracts/src/support/concerns/index.ts @@ -84,7 +84,7 @@ import Concern from "./Concern"; import ConcernConstructor from "./ConcernConstructor"; import Configuration from "./Configuration"; import Container from "./Container"; -import DescriptorsCache from "./DescriptorsCache"; +import DescriptorsRepository from "./DescriptorsRepository"; import Factory from "./Factory"; import Injector from "./Injector"; import RegistrationAware from "./RegistrationAware"; @@ -96,7 +96,7 @@ export { type ConcernConstructor, type Configuration, type Container, - type DescriptorsCache, + type DescriptorsRepository, type Factory, type Injector, type RegistrationAware, diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 7c87cb95..624e312f 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -6,7 +6,7 @@ import type { ShorthandConfiguration, Owner, Container, - DescriptorsCache, + DescriptorsRepository, Factory, Alias, Aliases, @@ -32,7 +32,7 @@ import InjectionError from './exceptions/InjectionError'; import UnsafeAliasError from './exceptions/UnsafeAliasError'; import ConcernsContainer from './ConcernsContainer'; import ConfigurationFactory from "./ConfigurationFactory"; -import Descriptors from "./Descriptors"; +import Repository from "./Repository"; import DescriptorFactory from "./DescriptorFactory"; import { isUnsafeKey } from "./isUnsafeKey"; @@ -72,13 +72,13 @@ export default class ConcernsInjector implements Injector protected factory: Factory; /** - * Descriptors Cache + * Descriptors Repository * - * @type {DescriptorsCache} + * @type {DescriptorsRepository} * * @protected */ - protected descriptors: DescriptorsCache; + protected repository: DescriptorsRepository; /** * Alias Descriptor Factory @@ -97,19 +97,19 @@ export default class ConcernsInjector implements Injector * @param {T} target The target class that concerns must be injected into * @param {Factory} [factory] * @param {AliasDescriptorFactory} [descriptorFactory] - * @param {DescriptorsCache} [descriptors] + * @param {DescriptorsRepository} [repository] */ public constructor( target: T, factory?: Factory, descriptorFactory?: AliasDescriptorFactory, - descriptors?: DescriptorsCache + repository?: DescriptorsRepository ) { this.#target = target; this.factory = factory || new ConfigurationFactory(); this.descriptorFactory = descriptorFactory || new DescriptorFactory(); - this.descriptors = descriptors || new Descriptors(); + this.repository = repository || new Repository(); } /** @@ -166,7 +166,7 @@ export default class ConcernsInjector implements Injector this.runAfterRegistration(modifiedTarget as UsesConcerns, modifiedTarget[CONCERN_CLASSES]); // Clear evt. cached items. - this.descriptors.clear(); + this.repository.clear(); // Finally, return the modified target return modifiedTarget; @@ -256,13 +256,13 @@ export default class ConcernsInjector implements Injector // Obtain previous applied aliases, form the target's parents. const appliedByParents: Map = this.getAllAppliedAliases(target as UsesConcerns); - this.descriptors.rememberDuring(target, () => { + this.repository.rememberDuring(target, () => { for (const configuration of configurations) { if (!configuration.allowAliases) { continue; } - this.descriptors.rememberDuring(configuration.concern, () => { + this.repository.rememberDuring(configuration.concern, () => { // Process the configuration aliases and define them. Merge returned aliases with the // applied aliases for the target class. const newApplied: Alias[] = this.processAliases( @@ -318,13 +318,13 @@ export default class ConcernsInjector implements Injector } // Skip if a property key already exists with same name as the "alias" - const targetDescriptors = this.descriptors.get(target); + const targetDescriptors = this.repository.get(target); if (Reflect.has(targetDescriptors, alias)) { return false; } // Abort if unable to find descriptor that matches given key in concern class. - const concernDescriptors = this.descriptors.get(source); + const concernDescriptors = this.repository.get(source); if (!Reflect.has(concernDescriptors, key)) { throw new InjectionError(target, source, `"${key.toString()}" does not exist in concern ${getNameOrDesc(source)} - attempted aliased as "${alias.toString()}" in target ${getNameOrDesc(target)}`); } diff --git a/packages/support/src/concerns/Descriptors.ts b/packages/support/src/concerns/Repository.ts similarity index 86% rename from packages/support/src/concerns/Descriptors.ts rename to packages/support/src/concerns/Repository.ts index fef4f976..4a4626c8 100644 --- a/packages/support/src/concerns/Descriptors.ts +++ b/packages/support/src/concerns/Repository.ts @@ -1,13 +1,13 @@ import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; -import type { ConcernConstructor, UsesConcerns, DescriptorsCache } from "@aedart/contracts/support/concerns"; +import type { ConcernConstructor, UsesConcerns, DescriptorsRepository } from "@aedart/contracts/support/concerns"; import { getClassPropertyDescriptors } from "@aedart/support/reflections"; /** - * Descriptors + * Repository * - * @see DescriptorsCache + * @see DescriptorsRepository */ -export default class Descriptors implements DescriptorsCache +export default class Repository implements DescriptorsRepository { /** * In-memory cache property descriptors for target class and concern classes @@ -16,7 +16,7 @@ export default class Descriptors implements DescriptorsCache * * @private */ - #cached: WeakMap< + #store: WeakMap< ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, Record >; @@ -25,9 +25,36 @@ export default class Descriptors implements DescriptorsCache * Create new Descriptors instance */ constructor() { - this.#cached = new WeakMap(); + this.#store = new WeakMap(); } + /** + * Returns property descriptors for given target class (recursively) + * + * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {boolean} [force=false] If `true` then method will not return evt. cached descriptors. + * @param {boolean} [cache=false] Caches the descriptors if `true`. + * + * @returns {Record} + */ + public get( + target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, + force: boolean = false, + cache: boolean = false + ): Record + { + if (!force && this.#store.has(target)) { + return this.#store.get(target) as Record; + } + + const descriptors = getClassPropertyDescriptors(target, true); + if (cache) { + this.#store.set(target, descriptors); + } + + return descriptors; + } + /** * Caches property descriptors for target during the execution of callback. * @@ -65,33 +92,6 @@ export default class Descriptors implements DescriptorsCache return this.get(target, force, true); } - /** - * Returns property descriptors for given target class (recursively) - * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class - * @param {boolean} [force=false] If `true` then method will not return evt. cached descriptors. - * @param {boolean} [cache=false] Caches the descriptors if `true`. - * - * @returns {Record} - */ - public get( - target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, - force: boolean = false, - cache: boolean = false - ): Record - { - if (!force && this.#cached.has(target)) { - return this.#cached.get(target) as Record; - } - - const descriptors = getClassPropertyDescriptors(target, true); - if (cache) { - this.#cached.set(target, descriptors); - } - - return descriptors; - } - /** * Deletes cached descriptors for target * @@ -101,7 +101,7 @@ export default class Descriptors implements DescriptorsCache */ public forget(target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor): boolean { - return this.#cached.delete(target); + return this.#store.delete(target); } /** @@ -111,7 +111,7 @@ export default class Descriptors implements DescriptorsCache */ public clear(): this { - this.#cached = new WeakMap(); + this.#store = new WeakMap(); return this; } diff --git a/packages/support/src/concerns/index.ts b/packages/support/src/concerns/index.ts index 9302695b..ca820314 100644 --- a/packages/support/src/concerns/index.ts +++ b/packages/support/src/concerns/index.ts @@ -3,7 +3,7 @@ import { ConcernClassBlueprint } from "./ConcernClassBlueprint"; import ConcernsContainer from "./ConcernsContainer"; import ConcernsInjector from "./ConcernsInjector"; import ConfigurationFactory from "./ConfigurationFactory"; -import Descriptors from "./Descriptors"; +import Repository from "./Repository"; import DescriptorFactory from "./DescriptorFactory"; export { @@ -12,7 +12,7 @@ export { ConcernsContainer, ConcernsInjector, ConfigurationFactory, - Descriptors, + Repository, DescriptorFactory }; From f8a852f8d7bf8e9af5a78ae1d0ea00dad1924038 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 09:46:43 +0100 Subject: [PATCH 393/424] Refactor, rename factory to configFactory --- packages/support/src/concerns/ConcernsInjector.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 624e312f..68093b5c 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -69,7 +69,7 @@ export default class ConcernsInjector implements Injector * * @protected */ - protected factory: Factory; + protected configFactory: Factory; /** * Descriptors Repository @@ -95,19 +95,19 @@ export default class ConcernsInjector implements Injector * @template T = object * * @param {T} target The target class that concerns must be injected into - * @param {Factory} [factory] + * @param {Factory} [configFactory] * @param {AliasDescriptorFactory} [descriptorFactory] * @param {DescriptorsRepository} [repository] */ public constructor( target: T, - factory?: Factory, + configFactory?: Factory, descriptorFactory?: AliasDescriptorFactory, repository?: DescriptorsRepository ) { this.#target = target; - this.factory = factory || new ConfigurationFactory(); + this.configFactory = configFactory || new ConfigurationFactory(); this.descriptorFactory = descriptorFactory || new DescriptorFactory(); this.repository = repository || new Repository(); } @@ -410,7 +410,7 @@ export default class ConcernsInjector implements Injector */ protected normaliseEntry(entry: ConcernConstructor|Configuration|ShorthandConfiguration): Configuration { - return this.factory.make(this.target as object, entry); + return this.configFactory.make(this.target as object, entry); } /** From eeefb923e0a53bd2654816b35da2ffbe789bccef Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 09:50:52 +0100 Subject: [PATCH 394/424] Refactor, rename runRegistraion... methods to callRegistration... --- .../support/src/concerns/ConcernsInjector.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 68093b5c..6d72ca7b 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -152,7 +152,7 @@ export default class ConcernsInjector implements Injector let modifiedTarget: UsesConcerns = this.defineConcerns(this.target, concernClasses); // Run before registration hook - this.runBeforeRegistration(this.target as UsesConcerns, modifiedTarget[CONCERN_CLASSES]); + this.callBeforeRegistration(this.target as UsesConcerns, modifiedTarget[CONCERN_CLASSES]); // Define concerns, container and aliases modifiedTarget = this.defineAliases( @@ -163,7 +163,7 @@ export default class ConcernsInjector implements Injector ); // Run after registration hook - this.runAfterRegistration(modifiedTarget as UsesConcerns, modifiedTarget[CONCERN_CLASSES]); + this.callAfterRegistration(modifiedTarget as UsesConcerns, modifiedTarget[CONCERN_CLASSES]); // Clear evt. cached items. this.repository.clear(); @@ -611,9 +611,9 @@ export default class ConcernsInjector implements Injector * * @throws {InjectionError} */ - protected runBeforeRegistration(target: UsesConcerns, concerns: ConcernConstructor[]): void + protected callBeforeRegistration(target: UsesConcerns, concerns: ConcernConstructor[]): void { - this.runRegistrationHook(target, concerns, BEFORE, 'Before'); + this.callRegistrationHook(target, concerns, BEFORE, 'Before'); } /** @@ -626,13 +626,13 @@ export default class ConcernsInjector implements Injector * * @throws {InjectionError} */ - protected runAfterRegistration(target: UsesConcerns, concerns: ConcernConstructor[]): void + protected callAfterRegistration(target: UsesConcerns, concerns: ConcernConstructor[]): void { - this.runRegistrationHook(target, concerns, AFTER, 'After'); + this.callRegistrationHook(target, concerns, AFTER, 'After'); } /** - * Run registration hook on given concern classes + * Invokes the registration hook in given concern classes * * @param {UsesConcerns} target * @param {ConcernConstructor} concerns @@ -643,7 +643,7 @@ export default class ConcernsInjector implements Injector * * @throws {InjectionError} */ - protected runRegistrationHook( + protected callRegistrationHook( target: UsesConcerns, concerns: ConcernConstructor[], hook: symbol, From bf2e567322841f87d3bc5ad0f64457a74f607f54 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 10:08:50 +0100 Subject: [PATCH 395/424] Use named import instead of default Default export of default theme has been deprecated. --- docs/.vuepress/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index d01a3eee..aeea721d 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -1,5 +1,5 @@ import {defineUserConfig, Page} from 'vuepress'; -import defaultTheme from "@vuepress/theme-default" +import { defaultTheme } from "@vuepress/theme-default" import { webpackBundler } from "@vuepress/bundler-webpack" import {backToTopPlugin} from "@vuepress/plugin-back-to-top"; import {searchPlugin} from "@vuepress/plugin-search"; From 6159a0b227b47f53de003421c9352343cfbf5bb6 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 10:16:48 +0100 Subject: [PATCH 396/424] Fix defineClientConfig() does not exist Appears that this util function was removed. --- docs/.vuepress/client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/.vuepress/client.ts b/docs/.vuepress/client.ts index 1f1dc711..f3889842 100644 --- a/docs/.vuepress/client.ts +++ b/docs/.vuepress/client.ts @@ -1,12 +1,12 @@ -import { defineClientConfig } from '@vuepress/client'; +import type { ClientConfig } from '@vuepress/client'; // @ts-ignore import Layout from "./layouts/Layout.vue"; -export default defineClientConfig({ +export default { enhance({ app, router, siteData }) {}, setup() {}, rootComponents: [], layouts: { Layout } -}) +} as ClientConfig; From a59326bb8eebbc1d1583e08753c93db6eac2d482 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 10:19:34 +0100 Subject: [PATCH 397/424] Change release notes --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fd36640..b6ce3782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Lodash JSDoc references in `get()`, `set()`, `unset()` and `forget()`, in `@aedart/support/objects`. * `jasmine-core` `v4.x` was used by `karma-jasmine`, which caused thrown exceptions that contained `ErrorOptions` to not being rendered correctly in the CLI. [_See GitHub issue for details_](https://github.com/jasmine/jasmine/issues/2028). +* Default `defaultTheme` deprecated, replaced with named import, in docs `config.ts`. +* `defineClientConfig()` does not exist in docs `client.ts` (_replaced with an object of the type `ClientConfig`_). ## [0.8.0] - 2024-02-12 From ba0452de53b436eed0568b93e465ad4987d5edfa Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 10:48:53 +0100 Subject: [PATCH 398/424] Change isSafeArrayLike to exclude plain strings values Also added tests for isSafeArrayLike --- .../support/src/arrays/isSafeArrayLike.ts | 14 +++++--- .../support/arrays/isArrayLike.test.js | 35 ++++++++++++++++++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/support/src/arrays/isSafeArrayLike.ts b/packages/support/src/arrays/isSafeArrayLike.ts index 3449cad3..92a0d1c3 100644 --- a/packages/support/src/arrays/isSafeArrayLike.ts +++ b/packages/support/src/arrays/isSafeArrayLike.ts @@ -5,15 +5,21 @@ import { isTypedArray } from './isTypedArray'; * Determine if value is "safe" array-like object * * **Note**: _In this context "safe" means that given object passes {@link isArrayLike}, - * but is not instance of a {@link String} object, nor is the object a [Typed Array]{@link isTypedArray}!_ + * but value is:_ + * - not a string. + * - not instance of a {@link String} object. + * - not a [Typed Array]{@link isTypedArray} object. * * @param {object} value * * @return {boolean} */ -export function isSafeArrayLike(value: object): boolean +export function isSafeArrayLike( + value: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +): boolean { - return isArrayLike(value) + return typeof value != 'string' && !(value instanceof String) - && !isTypedArray(value); + && !isTypedArray(value) + && isArrayLike(value); } \ No newline at end of file diff --git a/tests/browser/packages/support/arrays/isArrayLike.test.js b/tests/browser/packages/support/arrays/isArrayLike.test.js index fec57b07..a410de6d 100644 --- a/tests/browser/packages/support/arrays/isArrayLike.test.js +++ b/tests/browser/packages/support/arrays/isArrayLike.test.js @@ -1,4 +1,4 @@ -import { isArrayLike } from "@aedart/support/arrays"; +import { isArrayLike, isSafeArrayLike } from "@aedart/support/arrays"; describe('@aedart/support/arrays', () => { describe('isArrayLike()', () => { @@ -37,5 +37,38 @@ describe('@aedart/support/arrays', () => { .toBe(data.expected); } }); + + it('can determine if is "safe" array-like', () => { + + const dataSet = [ + { value: [], expected: true, name: 'Array' }, + { value: { length: 0 }, expected: true, name: 'Object (with length property)' }, + + // -------------------------------------------------------------------------------- // + { value: 'abc', expected: false, name: 'string' }, + { value: new String('abc'), expected: false, name: 'String (object)' }, + { value: new Int8Array(), expected: false, name: 'TypedArray' }, + + // -------------------------------------------------------------------------------- // + // These should never be considered array-like... + + { value: new Boolean(true), expected: false, name: 'Boolean' }, + { value: new Number(123), expected: false, name: 'Number' }, + { value: {}, expected: false, name: 'Object (without length property)' }, + { value: new Map(), expected: false, name: 'Map' }, + { value: new Set(), expected: false, name: 'Set' }, + { value: function() {}, expected: false, name: 'Function' }, + { value: new Date(), expected: false, name: 'Date' }, + { value: new ArrayBuffer(2), expected: false, name: 'ArrayBuffer' }, + { value: new DataView(new ArrayBuffer(2)), expected: false, name: 'DataView' }, + { value: new RegExp(/ab/g), expected: false, name: 'RegExp' }, + ]; + + for (const data of dataSet) { + expect(isSafeArrayLike(data.value)) + .withContext(`${data.name} was expected to ${data.expected.toString()}`) + .toBe(data.expected); + } + }); }); }); \ No newline at end of file From 6558713eb5f3fe3b8df7a4b96deef7b199e178ac Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 11:37:08 +0100 Subject: [PATCH 399/424] Add docs for support/arrays submodule --- docs/.vuepress/archive/Version0x.ts | 1 + .../current/packages/support/arrays.md | 193 ++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 docs/archive/current/packages/support/arrays.md diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index a65b3c06..0ad0e2b0 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -36,6 +36,7 @@ export default PagesCollection.make('v0.x', '/v0x', [ children: [ 'packages/support/', 'packages/support/install', + 'packages/support/arrays', 'packages/support/mixins', 'packages/support/meta', 'packages/support/objects', diff --git a/docs/archive/current/packages/support/arrays.md b/docs/archive/current/packages/support/arrays.md new file mode 100644 index 00000000..30db3795 --- /dev/null +++ b/docs/archive/current/packages/support/arrays.md @@ -0,0 +1,193 @@ +--- +title: Arrays +description: Array utilities. +sidebarDepth: 0 +--- + +# Arrays + +`@aedart/support/arrays` contains [array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) related utilities. + +[[TOC]] + +## `includesAll` + +Determines if an array includes all values. + +```js +import { includesAll } from '@aedart/support/arrays'; + +const arr = [ 1, 2, 3 ]; + +includesAll(arr, [ 1, 2 ]); // true +includesAll(arr, [ 1, 4 ]); // false +``` + +## `includesAny` + +Determines if an array includes some values. + +```js +import { includesAny } from '@aedart/support/arrays'; + +const arr = [ 1, 2, 3 ]; + +includesAll(arr, [ 4, 2 ]); // true +includesAll(arr, [ 5, 5 ]); // false +``` + +## `isArrayLike` + +Determines if a value is ["array-like"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#array-like_objects). + +(_`isArrayLike()` is an alias for Lodash's [isArrayLike](https://lodash.com/docs/4.17.15#isArrayLike)._) + +```js +import { isArrayLike } from '@aedart/support/arrays'; + +isArrayLike([]); // true +isArrayLike('abc'); // true +isArrayLike(new String('abc')); // true +isArrayLike({ length: 0 }); // true +isArrayLike(new Int8Array()); // true + +isArrayLike({}); // false +isArrayLike(function() {}); // false +isArrayLike(new Boolean(true)); // false +isArrayLike(123); // false +isArrayLike(new Number(123)); // false +// ...etc +``` + +See also [_`isSafeArrayLike()`_](#issafearraylike). + +## `isConcatSpreadable` + +Determines if object contains the [well-known](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#well-known_symbols) +symbol [`Symbol.isConcatSpreadable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable). + +```js +import { isConcatSpreadable } from '@aedart/support/arrays'; + +isConcatSpreadable(null); // false +isConcatSpreadable([ 1, 2, 3 ]); // false +isConcatSpreadable({}); // false + +// ------------------------------------------------------------------------- + +const arr = [ 1, 2, 3 ]; +arr[Symbol.isConcatSpreadable] = true; +isConcatSpreadable(arr); // true + +// ------------------------------------------------------------------------- + +const obj = { + [Symbol.isConcatSpreadable]: true, + + // NOTE: length should be present, if Symbol.isConcatSpreadable + // set to `true` However, isConcatSpreadable() does not check + // if `length` is set! + length: 3, + 0: 'a', + 1: 'b', + 2: 'c' +}; +isConcatSpreadable(obj); // true + +// ------------------------------------------------------------------------- + +class A {} +class B { + [Symbol.isConcatSpreadable] = false; +} +isConcatSpreadable(new A()); // false +isConcatSpreadable(new B()); // true +``` + +## `isSafeArrayLike` + +Determines if value is "safe" [array-like](#isarraylike) object. +In this context "safe" means that value is not a string, not an instance of [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/String) object, +and not a [Typed Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) object. + +```js +import { isSafeArrayLike } from '@aedart/support/arrays'; + +isSafeArrayLike([]); // true +isSafeArrayLike({ length: 0 }); // true + +isSafeArrayLike('abc'); // false +isSafeArrayLike(new String('abc')); // false +isSafeArrayLike(new Int8Array()); // false +// ...etc +``` + +## `isTypedArray` + +Determines if target is an instance of a [`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray). + +```js +import { isTypedArray } from '@aedart/support/arrays'; + +isTypedArray(null); // false +isTypedArray({}); // false +isTypedArray([]); // false +isTypedArray(new Map()); // false + +isTypedArray(new Int8Array()); // true +isTypedArray(new Uint8Array()); // true +isTypedArray(new Uint8ClampedArray()); // true +isTypedArray(new Int16Array()); // true +isTypedArray(new Uint16Array()); // true +isTypedArray(new Int32Array()); // true +isTypedArray(new Uint32Array()); // true +isTypedArray(new Float32Array()); // true +isTypedArray(new Float64Array()); // true +isTypedArray(new BigInt64Array()); // true +isTypedArray(new BigUint64Array()); // true +``` + +## `merge` + +Merges arrays into a new array. +The function attempts to deep copy values, using [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone). + +```js +import { merge } from '@aedart/support/arrays'; + +const a = [ 1, 2, 3 ]; +const b = [ 4, 5, 6 ]; +const c = [ 7, 8, 9 ]; + +merge(a, b, c); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ] +``` + +### Deep Copy Objects + +Simple (_or "plain"_) objects are not shallow copied. This means that new object instances returned as the resulting array's values. + +See [Mozilla's documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) for additional +information about what data types are supported. + +```js +const a = { foo: 'foo' }; +const b = { bar: 'bar' }; +const c = { ping: 'pong' }; + +const result = merge([ a ], [ b, c ]); + +console.log(result[0] === a); // false +console.log(result[1] === b); // false +console.log(result[2] === c); // false +``` + +### When unable to merge values + +In situations when values cannot be copied via `structuredClone`, an `ArrayMergeError` is thrown. + +```js +const a = [ 1, 2, 3 ]; +const b = [ function() {} ]; // A function cannot be deep copied... + +merge(a, b); // ArrayMergeError +``` \ No newline at end of file From 605fd33531931fafaff829ceb1d415ce09a317fa Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 12:37:44 +0100 Subject: [PATCH 400/424] Add test for when expression is given --- .../support/exceptions/getErrorMessage.test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/browser/packages/support/exceptions/getErrorMessage.test.js b/tests/browser/packages/support/exceptions/getErrorMessage.test.js index 9a94e44d..74682ee3 100644 --- a/tests/browser/packages/support/exceptions/getErrorMessage.test.js +++ b/tests/browser/packages/support/exceptions/getErrorMessage.test.js @@ -23,6 +23,21 @@ describe('@aedart/support/exceptions', () => { expect(result) .toBe(defaultMessage); }); + + it('defaults when expression is given', () => { + + let result; + const defaultMessage = 'Everyone just loves the saltyness of popcorn kebab brushd with sugar.'; + + try { + throw 'Some expression'; + } catch (e) { + result = getErrorMessage(e, defaultMessage); + } + + expect(result) + .toBe(defaultMessage); + }); }); }); \ No newline at end of file From a6d254fe78b69509fa6e7355286c552df20cc6a3 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 13:07:40 +0100 Subject: [PATCH 401/424] Add docs for support/exceptions submodule --- docs/.vuepress/archive/Version0x.ts | 3 +- .../current/packages/support/exceptions.md | 118 ++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 docs/archive/current/packages/support/exceptions.md diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index 0ad0e2b0..7b56efcf 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -37,8 +37,9 @@ export default PagesCollection.make('v0.x', '/v0x', [ 'packages/support/', 'packages/support/install', 'packages/support/arrays', - 'packages/support/mixins', + 'packages/support/exceptions', 'packages/support/meta', + 'packages/support/mixins', 'packages/support/objects', 'packages/support/reflections', 'packages/support/misc', diff --git a/docs/archive/current/packages/support/exceptions.md b/docs/archive/current/packages/support/exceptions.md new file mode 100644 index 00000000..7d7d3bdb --- /dev/null +++ b/docs/archive/current/packages/support/exceptions.md @@ -0,0 +1,118 @@ +--- +title: Exceptions +description: Custom Errors / Exceptions utilities. +sidebarDepth: 0 +--- + +# Exceptions + +`@aedart/support/exceptions` offers a few utilities for working with [Custom Errors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types). + +[[TOC]] + +## `configureCustomError` + +Configures a custom error by automatically setting the error's `name` property to the class' constructor name. +The function accepts the following arguments: + +* `error: Error` - the custom error instance +* `captureStackTrace: boolean = false` (_optional_) Captures and sets error's stack trace (_See [`configureStackTrace()`](#configurestacktrace) for details_). + +```js +import { configureCustomError } from "@aedart/support/exceptions"; + +class MyError extends Error { + constructor(message, options) { + super(message, options); + + configureCustomError(this); + } +} +``` + +See [Mozilla's documentation on Custom Error Types](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types) for additional information. + +## `configureStackTrace` + +Captures a new stack trace and sets given Error's [`stack`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack) property. +The function accepts an `Error` as argument. + +```js +import { configureStackTrace } from "@aedart/support/exceptions"; + +class MyError extends Error { + constructor(message, options) { + super(message, options); + + configureStackTrace(this); + } +} +``` + +::: warning +The `stack` is [not yet an official feature](https://github.com/tc39/proposal-error-stacks), even though it's supported by many major browsers and Node.js. +If you are working with custom errors, you might not need to capture and set the `stack` property. +Therefore, you should only use `configureStackTrace()` in situations when your JavaScript environment does not support stack traces in custom errors. +::: + +## `getErrorMessage` + +Returns an Error's `message`, if an [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) +instance is provided. Otherwise, a default message is returned. + +```js +import { getErrorMessage } from "@aedart/support/exceptions"; + +try { + throw new Error('Something went wrong!'); +} catch(e) { + const msg = getErrorMessage(e, 'unknown error'); // Something went wrong! +} + +// --------------------------------------------------------------------------- + +try { + throw 'Something went wrong!'; +} catch(e) { + const msg = getErrorMessage(e, 'unknown error'); // unknown error +} +``` + +## Custom Errors + +### `AbstractClassError` + +The `AbstractClassError` is intended to be thrown whenever an abstract class is attempted instantiated directly. + +```js +import { AbstractClassError } from "@aedart/support/exceptions"; + +/** + * @abstract + */ +class MyAbstractClass { + constructor() { + if (new.target === MyAbstractClass) { + throw new AbstractClassError(MyAbstractClass); + } + } +} + +const instance = new MyAbstractClass(); // AbstractClassError +``` + +### `LogicalError` + +To be thrown whenever there is an error in the programming logic. + +_Inspired by PHP's [`LogicException`](https://www.php.net/manual/en/class.logicexception)_ + +```js +import { LogicalError } from "@aedart/support/exceptions"; + +function print(person) { + if (printer === undefined) { + throw new LogicalError('Printer is missing, unable to print people'); + } +} +``` \ No newline at end of file From 774c0b2b7574cc916ca343a46eb4694de0a95a55 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 13:50:14 +0100 Subject: [PATCH 402/424] Improve description of keys parameter --- packages/support/src/objects/populate.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/support/src/objects/populate.ts b/packages/support/src/objects/populate.ts index dca7b7e4..d44f1ce6 100644 --- a/packages/support/src/objects/populate.ts +++ b/packages/support/src/objects/populate.ts @@ -1,5 +1,5 @@ -import { isKeySafe } from "@aedart/support/reflections"; -import type { SourceKeysCallback } from "@aedart/contracts/support/objects"; +import {isKeySafe} from "@aedart/support/reflections"; +import type {SourceKeysCallback} from "@aedart/contracts/support/objects"; /** * Populate target object with the properties from source object @@ -17,7 +17,8 @@ import type { SourceKeysCallback } from "@aedart/contracts/support/objects"; * @param {object} source * @param {PropertyKey | PropertyKey[] | SourceKeysCallback} [keys='*'] Keys to select and copy from `source` object. * If wildcard (`*`) given, then all properties from the `source` - * are selected. + * are selected. If a callback is given, then that callback must return + * key or keys to select from `source`. * @param {boolean} [safe=true] When `true`, properties must exist in target (_must be defined in target_), * before they are shallow copied. * From ff2a7f75577c919a004e03242ae8a73ffc992d3b Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 14:01:04 +0100 Subject: [PATCH 403/424] Add docs for isKeyUnsafe() --- .../current/packages/support/reflections.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/archive/current/packages/support/reflections.md b/docs/archive/current/packages/support/reflections.md index fb0b795d..74b09453 100644 --- a/docs/archive/current/packages/support/reflections.md +++ b/docs/archive/current/packages/support/reflections.md @@ -29,3 +29,25 @@ isConstructor(class {}); // true **Acknowledgement** The source code of the above shown methods is heavily inspired by Denis Pushkarev's Core-js implementation of the [Function.isCallable / Function.isConstructor](https://github.com/zloirock/core-js#function-iscallable-isconstructor-) proposal (_License MIT_). + +## `isKeyUnsafe` + +Determines if a property key is considered "unsafe". + +```js +import { isKeyUnsafe } from '@aedart/support/reflections'; + +isKeyUnsafe('name'); // true +isKeyUnsafe('length'); // true +isKeyUnsafe('constructor'); // true +isKeyUnsafe('__proto__'); // false +``` + +::: tip Note +Behind the scene, the `isKeyUnsafe()` function matches the given key against values from the predefined `DANGEROUS_PROPERTIES` list, +which is defined in the `@aedart/contracts/support/objects` submodule; + +```js +import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; +``` +::: \ No newline at end of file From 4f8c727c2fea6dc93fb9529c53424edac8674cc1 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 29 Feb 2024 14:32:33 +0100 Subject: [PATCH 404/424] Add docs for isCloneable(), isPopulatable() and populate() --- .../current/packages/support/objects.md | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/docs/archive/current/packages/support/objects.md b/docs/archive/current/packages/support/objects.md index 5fc889df..aa66222b 100644 --- a/docs/archive/current/packages/support/objects.md +++ b/docs/archive/current/packages/support/objects.md @@ -197,6 +197,58 @@ const target = { console.log(hasUniqueId(target)); // false ``` +## `isCloneable` + +Determines if given object is "cloneable". +In this context "cloneable" means that an object implements the `Cloneable` interface, and offers a `clone()` method. + +_See `@aedart/constracts/support/objects/Cloneable` for details._ + +```js +import { isCloneable } from "@aedart/support/objects"; + +class A {}; + +class B { + clone() { + return new this(); + } +} + +isCloneable(null); // false +isCloneable([]); // false +isCloneable({}); // false +isCloneable(new A()); // false +isCloneable(new B()); // true +``` + +## `isPopulatable` + +Determines if given object is "populatable". +Here, "populatable" means that an object implements the `Populatable` interface, and offers a `populate()` method. + +_See `@aedart/constracts/support/objects/Populatable` for details._ + +```js +import { isPopulatable } from "@aedart/support/objects"; + +class A {}; + +class B { + populate(data) { + // ...not shown here... + + return this; + } +} + +isPopulatable(null); // false +isPopulatable([]); // false +isPopulatable({}); // false +isPopulatable(new A()); // false +isPopulatable(new B()); // true +``` + ## `isset` Determine if paths are properties of given object and have values. @@ -232,6 +284,122 @@ console.log(isset(target, 'b.c', 'b.name')); // false console.log(isset(target, 'a', 'b.name', 'b.c.age')); // false ``` +## `populate` + +The `populate()` allows you to populate a target object's properties with those from a source object. +The values are [shallow copied](https://developer.mozilla.org/en-US/docs/Glossary/Shallow_copy). +It accepts the following arguments: + +* `target: object` + +* `source: object` + +* `keys: PropertyKey | PropertyKey[] | SourceKeysCallback = '*'` - The keys to select and copy from `source` object. If wildcard (`*`) given, then all properties from the `source` are selected. + If a callback is given, then that callback must return key or keys to select from `source`. + +* `safe: boolean = true` - When `true`, properties must exist in target (_must be defined in target_), before they are shallow copied. + +::: warning Caution +The `target` object is mutated by this function. +::: + +::: tip Note +["Unsafe" properties](./reflections.md#iskeyunsafe) are disregarded, regardless of what `keys` are given. +::: + +```js +import { populate } from "@aedart/support/objects"; + +class Person { + name = null; + age = null; + + constructor(data) { + populate(this, data); + } +} + +const instance = new Person({ name: 'Janine', age: 36 }); +instance.name // Janine +instance.age // 36 +``` + +### Limit keys to populate + +By default, all keys (_`*`_) from the `source` object are attempted populated into the `target`. +You can limit what properties can be populated, by specifying what keys are allowed to be populated. + +```js +class Person { + name = null; + age = null; + phone = null; + + constructor(data) { + populate(this, data, [ 'name', 'age' ]); + } +} + +const instance = new Person({ name: 'Janine', age: 36, phone: '555 555 555' }); +instance.name // Janine +instance.age // 36 +instance.phone // null +``` + +### Source Keys Callback + +If you need a more advanced way to determine what keys to populate, then you can specify a callback as the `keys` argument. + +```js +populate(target, source, (source, target) => { + if (Reflect.has(source, 'phone') && Reflect.has(target, 'phone')) { + return [ 'name', 'age', 'phone' ]; + } + + return [ 'name', 'age' ]; +}); +``` + +### When keys do not exist + +When the `safe` argument is set to `true` (_default behavior_), and a property key does not exist in the `target` object, +then a `TypeError` is thrown. + +```js +class Person { + name = null; + age = null; + + constructor(data) { + populate(this, data, [ 'name', 'age', 'phone' ]); + } +} + +const instance = new Person({ + name: 'Janine', + age: 36, + phone: '555 555 555' +}); // TypeError - phone does not exist in target +``` + +However, if a requested key does not exist in the source object, then a `TypeError` is thrown regardless of the `safe` argument value. + +```js +class Person { + name = null; + age = null; + + constructor(data) { + populate(this, data, [ 'name', 'age', 'phone' ], false); + } +} + +const instance = new Person({ + name: 'Janine', + age: 36 +}); // TypeError - phone does not exist in source +``` + ## `set` Set a value in object at given path. From 599de39c9081b0f8fc661d34dc11129bdc81b5fa Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 1 Mar 2024 11:25:21 +0100 Subject: [PATCH 405/424] Refactor docs, split each submodule into own directory Also, extracted each util function into own page. This was needed because some of the submodule docs where getting very large and unmaintainable. --- docs/.vuepress/archive/Version0x.ts | 102 +++- .../current/packages/support/arrays.md | 193 -------- .../current/packages/support/arrays/README.md | 8 + .../packages/support/arrays/includesAll.md | 18 + .../packages/support/arrays/includesAny.md | 18 + .../packages/support/arrays/isArrayLike.md | 30 ++ .../support/arrays/isConcatSpreadable.md | 48 ++ .../support/arrays/isSafeArrayLike.md | 23 + .../packages/support/arrays/isTypedArray.md | 30 ++ .../current/packages/support/arrays/merge.md | 51 ++ .../current/packages/support/exceptions.md | 118 ----- .../packages/support/exceptions/README.md | 8 + .../exceptions/configureCustomError.md | 32 ++ .../support/exceptions/configureStackTrace.md | 31 ++ .../support/exceptions/customErrors.md | 46 ++ .../support/exceptions/getErrorMessage.md | 28 ++ docs/archive/current/packages/support/meta.md | 426 ----------------- .../current/packages/support/meta/README.md | 18 + .../packages/support/meta/inheritance.md | 51 ++ .../packages/support/meta/outsideChanges.md | 29 ++ .../packages/support/meta/prerequisites.md | 10 + .../packages/support/meta/setAndGet.md | 123 +++++ .../packages/support/meta/supported.md | 18 + .../packages/support/meta/targetMeta.md | 186 ++++++++ .../current/packages/support/meta/tc39.md | 40 ++ docs/archive/current/packages/support/misc.md | 199 -------- .../current/packages/support/misc/README.md | 8 + .../current/packages/support/misc/descTag.md | 28 ++ .../current/packages/support/misc/empty.md | 53 +++ .../current/packages/support/misc/isKey.md | 24 + .../packages/support/misc/isPrimitive.md | 25 + .../packages/support/misc/isPropertyKey.md | 23 + .../current/packages/support/misc/isset.md | 41 ++ .../packages/support/misc/mergeKeys.md | 17 + .../packages/support/misc/toWeakRef.md | 21 + .../current/packages/support/mixins.md | 234 ---------- .../current/packages/support/mixins/README.md | 41 ++ .../current/packages/support/mixins/apply.md | 56 +++ .../packages/support/mixins/inheritance.md | 63 +++ .../packages/support/mixins/instanceof.md | 40 ++ .../packages/support/mixins/newMixin.md | 44 ++ .../current/packages/support/mixins/onward.md | 13 + .../current/packages/support/objects.md | 438 ------------------ .../packages/support/objects/README.md | 8 + .../packages/support/objects/forget.md | 27 ++ .../packages/support/objects/forgetAll.md | 26 ++ .../current/packages/support/objects/get.md | 47 ++ .../current/packages/support/objects/has.md | 28 ++ .../packages/support/objects/hasAll.md | 43 ++ .../packages/support/objects/hasAny.md | 32 ++ .../packages/support/objects/hasUniqueId.md | 21 + .../packages/support/objects/isCloneable.md | 30 ++ .../packages/support/objects/isPopulatable.md | 32 ++ .../current/packages/support/objects/isset.md | 40 ++ .../packages/support/objects/populate.md | 126 +++++ .../current/packages/support/objects/set.md | 20 + .../packages/support/objects/uniqueId.md | 27 ++ .../current/packages/support/reflections.md | 53 --- .../packages/support/reflections/README.md | 9 + .../support/reflections/isConstructor.md | 25 + .../support/reflections/isKeyUnsafe.md | 27 ++ 61 files changed, 2006 insertions(+), 1668 deletions(-) delete mode 100644 docs/archive/current/packages/support/arrays.md create mode 100644 docs/archive/current/packages/support/arrays/README.md create mode 100644 docs/archive/current/packages/support/arrays/includesAll.md create mode 100644 docs/archive/current/packages/support/arrays/includesAny.md create mode 100644 docs/archive/current/packages/support/arrays/isArrayLike.md create mode 100644 docs/archive/current/packages/support/arrays/isConcatSpreadable.md create mode 100644 docs/archive/current/packages/support/arrays/isSafeArrayLike.md create mode 100644 docs/archive/current/packages/support/arrays/isTypedArray.md create mode 100644 docs/archive/current/packages/support/arrays/merge.md delete mode 100644 docs/archive/current/packages/support/exceptions.md create mode 100644 docs/archive/current/packages/support/exceptions/README.md create mode 100644 docs/archive/current/packages/support/exceptions/configureCustomError.md create mode 100644 docs/archive/current/packages/support/exceptions/configureStackTrace.md create mode 100644 docs/archive/current/packages/support/exceptions/customErrors.md create mode 100644 docs/archive/current/packages/support/exceptions/getErrorMessage.md delete mode 100644 docs/archive/current/packages/support/meta.md create mode 100644 docs/archive/current/packages/support/meta/README.md create mode 100644 docs/archive/current/packages/support/meta/inheritance.md create mode 100644 docs/archive/current/packages/support/meta/outsideChanges.md create mode 100644 docs/archive/current/packages/support/meta/prerequisites.md create mode 100644 docs/archive/current/packages/support/meta/setAndGet.md create mode 100644 docs/archive/current/packages/support/meta/supported.md create mode 100644 docs/archive/current/packages/support/meta/targetMeta.md create mode 100644 docs/archive/current/packages/support/meta/tc39.md delete mode 100644 docs/archive/current/packages/support/misc.md create mode 100644 docs/archive/current/packages/support/misc/README.md create mode 100644 docs/archive/current/packages/support/misc/descTag.md create mode 100644 docs/archive/current/packages/support/misc/empty.md create mode 100644 docs/archive/current/packages/support/misc/isKey.md create mode 100644 docs/archive/current/packages/support/misc/isPrimitive.md create mode 100644 docs/archive/current/packages/support/misc/isPropertyKey.md create mode 100644 docs/archive/current/packages/support/misc/isset.md create mode 100644 docs/archive/current/packages/support/misc/mergeKeys.md create mode 100644 docs/archive/current/packages/support/misc/toWeakRef.md delete mode 100644 docs/archive/current/packages/support/mixins.md create mode 100644 docs/archive/current/packages/support/mixins/README.md create mode 100644 docs/archive/current/packages/support/mixins/apply.md create mode 100644 docs/archive/current/packages/support/mixins/inheritance.md create mode 100644 docs/archive/current/packages/support/mixins/instanceof.md create mode 100644 docs/archive/current/packages/support/mixins/newMixin.md create mode 100644 docs/archive/current/packages/support/mixins/onward.md delete mode 100644 docs/archive/current/packages/support/objects.md create mode 100644 docs/archive/current/packages/support/objects/README.md create mode 100644 docs/archive/current/packages/support/objects/forget.md create mode 100644 docs/archive/current/packages/support/objects/forgetAll.md create mode 100644 docs/archive/current/packages/support/objects/get.md create mode 100644 docs/archive/current/packages/support/objects/has.md create mode 100644 docs/archive/current/packages/support/objects/hasAll.md create mode 100644 docs/archive/current/packages/support/objects/hasAny.md create mode 100644 docs/archive/current/packages/support/objects/hasUniqueId.md create mode 100644 docs/archive/current/packages/support/objects/isCloneable.md create mode 100644 docs/archive/current/packages/support/objects/isPopulatable.md create mode 100644 docs/archive/current/packages/support/objects/isset.md create mode 100644 docs/archive/current/packages/support/objects/populate.md create mode 100644 docs/archive/current/packages/support/objects/set.md create mode 100644 docs/archive/current/packages/support/objects/uniqueId.md delete mode 100644 docs/archive/current/packages/support/reflections.md create mode 100644 docs/archive/current/packages/support/reflections/README.md create mode 100644 docs/archive/current/packages/support/reflections/isConstructor.md create mode 100644 docs/archive/current/packages/support/reflections/isKeyUnsafe.md diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index 7b56efcf..d34a11ab 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -36,13 +36,101 @@ export default PagesCollection.make('v0.x', '/v0x', [ children: [ 'packages/support/', 'packages/support/install', - 'packages/support/arrays', - 'packages/support/exceptions', - 'packages/support/meta', - 'packages/support/mixins', - 'packages/support/objects', - 'packages/support/reflections', - 'packages/support/misc', + { + text: 'Arrays', + collapsible: true, + children: [ + 'packages/support/arrays/', + 'packages/support/arrays/includesAll', + 'packages/support/arrays/includesAny', + 'packages/support/arrays/isArrayLike', + 'packages/support/arrays/isConcatSpreadable', + 'packages/support/arrays/isSafeArrayLike', + 'packages/support/arrays/isTypedArray', + 'packages/support/arrays/merge', + ] + }, + { + text: 'Exceptions', + collapsible: true, + children: [ + 'packages/support/exceptions/', + 'packages/support/exceptions/configureCustomError', + 'packages/support/exceptions/configureStackTrace', + 'packages/support/exceptions/getErrorMessage', + 'packages/support/exceptions/customErrors', + ] + }, + { + text: 'Meta', + collapsible: true, + children: [ + 'packages/support/meta/', + 'packages/support/meta/prerequisites', + 'packages/support/meta/supported', + 'packages/support/meta/setAndGet', + 'packages/support/meta/inheritance', + 'packages/support/meta/outsideChanges', + 'packages/support/meta/tc39', + 'packages/support/meta/targetMeta', + ] + }, + { + text: 'Mixins', + collapsible: true, + children: [ + 'packages/support/mixins/', + 'packages/support/mixins/newMixin', + 'packages/support/mixins/apply', + 'packages/support/mixins/instanceof', + 'packages/support/mixins/inheritance', + 'packages/support/mixins/onward', + ] + }, + { + text: 'Object', + collapsible: true, + children: [ + 'packages/support/objects/', + 'packages/support/objects/forget', + 'packages/support/objects/forgetAll', + 'packages/support/objects/get', + 'packages/support/objects/has', + 'packages/support/objects/hasAll', + 'packages/support/objects/hasAny', + 'packages/support/objects/hasUniqueId', + 'packages/support/objects/isCloneable', + 'packages/support/objects/isPopulatable', + 'packages/support/objects/isset', + 'packages/support/objects/populate', + 'packages/support/objects/set', + 'packages/support/objects/uniqueId', + ] + }, + { + text: 'Reflections', + collapsible: true, + children: [ + 'packages/support/reflections/', + 'packages/support/reflections/isConstructor', + 'packages/support/reflections/isKeyUnsafe', + ] + }, + { + text: 'Misc', + collapsible: true, + children: [ + 'packages/support/misc/', + 'packages/support/misc/descTag', + 'packages/support/misc/empty', + 'packages/support/misc/isKey', + 'packages/support/misc/isPrimitive', + 'packages/support/misc/isPropertyKey', + 'packages/support/misc/isset', + 'packages/support/misc/mergeKeys', + 'packages/support/misc/toWeakRef', + ] + }, ] }, { diff --git a/docs/archive/current/packages/support/arrays.md b/docs/archive/current/packages/support/arrays.md deleted file mode 100644 index 30db3795..00000000 --- a/docs/archive/current/packages/support/arrays.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -title: Arrays -description: Array utilities. -sidebarDepth: 0 ---- - -# Arrays - -`@aedart/support/arrays` contains [array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) related utilities. - -[[TOC]] - -## `includesAll` - -Determines if an array includes all values. - -```js -import { includesAll } from '@aedart/support/arrays'; - -const arr = [ 1, 2, 3 ]; - -includesAll(arr, [ 1, 2 ]); // true -includesAll(arr, [ 1, 4 ]); // false -``` - -## `includesAny` - -Determines if an array includes some values. - -```js -import { includesAny } from '@aedart/support/arrays'; - -const arr = [ 1, 2, 3 ]; - -includesAll(arr, [ 4, 2 ]); // true -includesAll(arr, [ 5, 5 ]); // false -``` - -## `isArrayLike` - -Determines if a value is ["array-like"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#array-like_objects). - -(_`isArrayLike()` is an alias for Lodash's [isArrayLike](https://lodash.com/docs/4.17.15#isArrayLike)._) - -```js -import { isArrayLike } from '@aedart/support/arrays'; - -isArrayLike([]); // true -isArrayLike('abc'); // true -isArrayLike(new String('abc')); // true -isArrayLike({ length: 0 }); // true -isArrayLike(new Int8Array()); // true - -isArrayLike({}); // false -isArrayLike(function() {}); // false -isArrayLike(new Boolean(true)); // false -isArrayLike(123); // false -isArrayLike(new Number(123)); // false -// ...etc -``` - -See also [_`isSafeArrayLike()`_](#issafearraylike). - -## `isConcatSpreadable` - -Determines if object contains the [well-known](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#well-known_symbols) -symbol [`Symbol.isConcatSpreadable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable). - -```js -import { isConcatSpreadable } from '@aedart/support/arrays'; - -isConcatSpreadable(null); // false -isConcatSpreadable([ 1, 2, 3 ]); // false -isConcatSpreadable({}); // false - -// ------------------------------------------------------------------------- - -const arr = [ 1, 2, 3 ]; -arr[Symbol.isConcatSpreadable] = true; -isConcatSpreadable(arr); // true - -// ------------------------------------------------------------------------- - -const obj = { - [Symbol.isConcatSpreadable]: true, - - // NOTE: length should be present, if Symbol.isConcatSpreadable - // set to `true` However, isConcatSpreadable() does not check - // if `length` is set! - length: 3, - 0: 'a', - 1: 'b', - 2: 'c' -}; -isConcatSpreadable(obj); // true - -// ------------------------------------------------------------------------- - -class A {} -class B { - [Symbol.isConcatSpreadable] = false; -} -isConcatSpreadable(new A()); // false -isConcatSpreadable(new B()); // true -``` - -## `isSafeArrayLike` - -Determines if value is "safe" [array-like](#isarraylike) object. -In this context "safe" means that value is not a string, not an instance of [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/String) object, -and not a [Typed Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) object. - -```js -import { isSafeArrayLike } from '@aedart/support/arrays'; - -isSafeArrayLike([]); // true -isSafeArrayLike({ length: 0 }); // true - -isSafeArrayLike('abc'); // false -isSafeArrayLike(new String('abc')); // false -isSafeArrayLike(new Int8Array()); // false -// ...etc -``` - -## `isTypedArray` - -Determines if target is an instance of a [`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray). - -```js -import { isTypedArray } from '@aedart/support/arrays'; - -isTypedArray(null); // false -isTypedArray({}); // false -isTypedArray([]); // false -isTypedArray(new Map()); // false - -isTypedArray(new Int8Array()); // true -isTypedArray(new Uint8Array()); // true -isTypedArray(new Uint8ClampedArray()); // true -isTypedArray(new Int16Array()); // true -isTypedArray(new Uint16Array()); // true -isTypedArray(new Int32Array()); // true -isTypedArray(new Uint32Array()); // true -isTypedArray(new Float32Array()); // true -isTypedArray(new Float64Array()); // true -isTypedArray(new BigInt64Array()); // true -isTypedArray(new BigUint64Array()); // true -``` - -## `merge` - -Merges arrays into a new array. -The function attempts to deep copy values, using [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone). - -```js -import { merge } from '@aedart/support/arrays'; - -const a = [ 1, 2, 3 ]; -const b = [ 4, 5, 6 ]; -const c = [ 7, 8, 9 ]; - -merge(a, b, c); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ] -``` - -### Deep Copy Objects - -Simple (_or "plain"_) objects are not shallow copied. This means that new object instances returned as the resulting array's values. - -See [Mozilla's documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) for additional -information about what data types are supported. - -```js -const a = { foo: 'foo' }; -const b = { bar: 'bar' }; -const c = { ping: 'pong' }; - -const result = merge([ a ], [ b, c ]); - -console.log(result[0] === a); // false -console.log(result[1] === b); // false -console.log(result[2] === c); // false -``` - -### When unable to merge values - -In situations when values cannot be copied via `structuredClone`, an `ArrayMergeError` is thrown. - -```js -const a = [ 1, 2, 3 ]; -const b = [ function() {} ]; // A function cannot be deep copied... - -merge(a, b); // ArrayMergeError -``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/arrays/README.md b/docs/archive/current/packages/support/arrays/README.md new file mode 100644 index 00000000..72426676 --- /dev/null +++ b/docs/archive/current/packages/support/arrays/README.md @@ -0,0 +1,8 @@ +--- +title: About Arrays +description: Array utilities. +--- + +# About Arrays + +`@aedart/support/arrays` contains [array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) related utilities. \ No newline at end of file diff --git a/docs/archive/current/packages/support/arrays/includesAll.md b/docs/archive/current/packages/support/arrays/includesAll.md new file mode 100644 index 00000000..6a30719f --- /dev/null +++ b/docs/archive/current/packages/support/arrays/includesAll.md @@ -0,0 +1,18 @@ +--- +title: Includes All +description: Determine if array contains all values. +sidebarDepth: 0 +--- + +# `includesAll` + +Determines if an array includes all values. + +```js +import { includesAll } from '@aedart/support/arrays'; + +const arr = [ 1, 2, 3 ]; + +includesAll(arr, [ 1, 2 ]); // true +includesAll(arr, [ 1, 4 ]); // false +``` diff --git a/docs/archive/current/packages/support/arrays/includesAny.md b/docs/archive/current/packages/support/arrays/includesAny.md new file mode 100644 index 00000000..02aa63e5 --- /dev/null +++ b/docs/archive/current/packages/support/arrays/includesAny.md @@ -0,0 +1,18 @@ +--- +title: Includes Any +description: Determine if array contains some values. +sidebarDepth: 0 +--- + +# `includesAny` + +Determines if an array includes some values. + +```js +import { includesAny } from '@aedart/support/arrays'; + +const arr = [ 1, 2, 3 ]; + +includesAll(arr, [ 4, 2 ]); // true +includesAll(arr, [ 5, 5 ]); // false +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/arrays/isArrayLike.md b/docs/archive/current/packages/support/arrays/isArrayLike.md new file mode 100644 index 00000000..1d7961fc --- /dev/null +++ b/docs/archive/current/packages/support/arrays/isArrayLike.md @@ -0,0 +1,30 @@ +--- +title: Is Array Like +description: Determine if value is array-like. +sidebarDepth: 0 +--- + +# `isArrayLike` + +Determines if a value is ["array-like"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#array-like_objects). + +(_`isArrayLike()` is an alias for Lodash's [isArrayLike](https://lodash.com/docs/4.17.15#isArrayLike)._) + +```js +import { isArrayLike } from '@aedart/support/arrays'; + +isArrayLike([]); // true +isArrayLike('abc'); // true +isArrayLike(new String('abc')); // true +isArrayLike({ length: 0 }); // true +isArrayLike(new Int8Array()); // true + +isArrayLike({}); // false +isArrayLike(function() {}); // false +isArrayLike(new Boolean(true)); // false +isArrayLike(123); // false +isArrayLike(new Number(123)); // false +// ...etc +``` + +See also [_`isSafeArrayLike()`_](./issafearraylike.md). \ No newline at end of file diff --git a/docs/archive/current/packages/support/arrays/isConcatSpreadable.md b/docs/archive/current/packages/support/arrays/isConcatSpreadable.md new file mode 100644 index 00000000..4dfc2dd0 --- /dev/null +++ b/docs/archive/current/packages/support/arrays/isConcatSpreadable.md @@ -0,0 +1,48 @@ +--- +title: Is Concat Spreadable +description: Determine if object is concat spreadable. +sidebarDepth: 0 +--- + +# `isConcatSpreadable` + +Determines if object contains the [well-known](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol#well-known_symbols) +symbol [`Symbol.isConcatSpreadable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable). + +```js +import { isConcatSpreadable } from '@aedart/support/arrays'; + +isConcatSpreadable(null); // false +isConcatSpreadable([ 1, 2, 3 ]); // false +isConcatSpreadable({}); // false + +// ------------------------------------------------------------------------- + +const arr = [ 1, 2, 3 ]; +arr[Symbol.isConcatSpreadable] = true; +isConcatSpreadable(arr); // true + +// ------------------------------------------------------------------------- + +const obj = { + [Symbol.isConcatSpreadable]: true, + + // NOTE: length should be present, if Symbol.isConcatSpreadable + // set to `true` However, isConcatSpreadable() does not check + // if `length` is set! + length: 3, + 0: 'a', + 1: 'b', + 2: 'c' +}; +isConcatSpreadable(obj); // true + +// ------------------------------------------------------------------------- + +class A {} +class B { + [Symbol.isConcatSpreadable] = false; +} +isConcatSpreadable(new A()); // false +isConcatSpreadable(new B()); // true +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/arrays/isSafeArrayLike.md b/docs/archive/current/packages/support/arrays/isSafeArrayLike.md new file mode 100644 index 00000000..895bdf44 --- /dev/null +++ b/docs/archive/current/packages/support/arrays/isSafeArrayLike.md @@ -0,0 +1,23 @@ +--- +title: Is Safe Array Like +description: Determine if value is "safe" array-like. +sidebarDepth: 0 +--- + +# `isSafeArrayLike` + +Determines if value is "safe" [array-like](#isarraylike) object. +In this context "safe" means that value is not a string, not an instance of [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/String) object, +and not a [Typed Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) object. + +```js +import { isSafeArrayLike } from '@aedart/support/arrays'; + +isSafeArrayLike([]); // true +isSafeArrayLike({ length: 0 }); // true + +isSafeArrayLike('abc'); // false +isSafeArrayLike(new String('abc')); // false +isSafeArrayLike(new Int8Array()); // false +// ...etc +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/arrays/isTypedArray.md b/docs/archive/current/packages/support/arrays/isTypedArray.md new file mode 100644 index 00000000..3c4835ef --- /dev/null +++ b/docs/archive/current/packages/support/arrays/isTypedArray.md @@ -0,0 +1,30 @@ +--- +title: Is Typed Array +description: Determine if object is a typed array. +sidebarDepth: 0 +--- + +# `isTypedArray` + +Determines if target is an instance of a [`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray). + +```js +import { isTypedArray } from '@aedart/support/arrays'; + +isTypedArray(null); // false +isTypedArray({}); // false +isTypedArray([]); // false +isTypedArray(new Map()); // false + +isTypedArray(new Int8Array()); // true +isTypedArray(new Uint8Array()); // true +isTypedArray(new Uint8ClampedArray()); // true +isTypedArray(new Int16Array()); // true +isTypedArray(new Uint16Array()); // true +isTypedArray(new Int32Array()); // true +isTypedArray(new Uint32Array()); // true +isTypedArray(new Float32Array()); // true +isTypedArray(new Float64Array()); // true +isTypedArray(new BigInt64Array()); // true +isTypedArray(new BigUint64Array()); // true +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/arrays/merge.md b/docs/archive/current/packages/support/arrays/merge.md new file mode 100644 index 00000000..c7a54b64 --- /dev/null +++ b/docs/archive/current/packages/support/arrays/merge.md @@ -0,0 +1,51 @@ +--- +title: Merge +description: Merge multiple arrays into a new array. +sidebarDepth: 0 +--- + +# `merge` + +Merges arrays into a new array. +This function attempts to deep copy values, using [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone). + +```js +import { merge } from '@aedart/support/arrays'; + +const a = [ 1, 2, 3 ]; +const b = [ 4, 5, 6 ]; +const c = [ 7, 8, 9 ]; + +merge(a, b, c); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ] +``` + +## Deep Copy Objects + +Simple (_or "plain"_) objects [deep copied](https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy). +This means that new objects are returned in the resulting array. + +See [Mozilla's documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) for additional +information about what data types are supported. + +```js +const a = { foo: 'foo' }; +const b = { bar: 'bar' }; +const c = { ping: 'pong' }; + +const result = merge([ a ], [ b, c ]); + +console.log(result[0] === a); // false +console.log(result[1] === b); // false +console.log(result[2] === c); // false +``` + +## When unable to merge values + +In situations when values cannot be copied via `structuredClone`, an `ArrayMergeError` is thrown. + +```js +const a = [ 1, 2, 3 ]; +const b = [ function() {} ]; // A function cannot be deep copied... + +merge(a, b); // ArrayMergeError +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/exceptions.md b/docs/archive/current/packages/support/exceptions.md deleted file mode 100644 index 7d7d3bdb..00000000 --- a/docs/archive/current/packages/support/exceptions.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -title: Exceptions -description: Custom Errors / Exceptions utilities. -sidebarDepth: 0 ---- - -# Exceptions - -`@aedart/support/exceptions` offers a few utilities for working with [Custom Errors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types). - -[[TOC]] - -## `configureCustomError` - -Configures a custom error by automatically setting the error's `name` property to the class' constructor name. -The function accepts the following arguments: - -* `error: Error` - the custom error instance -* `captureStackTrace: boolean = false` (_optional_) Captures and sets error's stack trace (_See [`configureStackTrace()`](#configurestacktrace) for details_). - -```js -import { configureCustomError } from "@aedart/support/exceptions"; - -class MyError extends Error { - constructor(message, options) { - super(message, options); - - configureCustomError(this); - } -} -``` - -See [Mozilla's documentation on Custom Error Types](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types) for additional information. - -## `configureStackTrace` - -Captures a new stack trace and sets given Error's [`stack`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack) property. -The function accepts an `Error` as argument. - -```js -import { configureStackTrace } from "@aedart/support/exceptions"; - -class MyError extends Error { - constructor(message, options) { - super(message, options); - - configureStackTrace(this); - } -} -``` - -::: warning -The `stack` is [not yet an official feature](https://github.com/tc39/proposal-error-stacks), even though it's supported by many major browsers and Node.js. -If you are working with custom errors, you might not need to capture and set the `stack` property. -Therefore, you should only use `configureStackTrace()` in situations when your JavaScript environment does not support stack traces in custom errors. -::: - -## `getErrorMessage` - -Returns an Error's `message`, if an [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) -instance is provided. Otherwise, a default message is returned. - -```js -import { getErrorMessage } from "@aedart/support/exceptions"; - -try { - throw new Error('Something went wrong!'); -} catch(e) { - const msg = getErrorMessage(e, 'unknown error'); // Something went wrong! -} - -// --------------------------------------------------------------------------- - -try { - throw 'Something went wrong!'; -} catch(e) { - const msg = getErrorMessage(e, 'unknown error'); // unknown error -} -``` - -## Custom Errors - -### `AbstractClassError` - -The `AbstractClassError` is intended to be thrown whenever an abstract class is attempted instantiated directly. - -```js -import { AbstractClassError } from "@aedart/support/exceptions"; - -/** - * @abstract - */ -class MyAbstractClass { - constructor() { - if (new.target === MyAbstractClass) { - throw new AbstractClassError(MyAbstractClass); - } - } -} - -const instance = new MyAbstractClass(); // AbstractClassError -``` - -### `LogicalError` - -To be thrown whenever there is an error in the programming logic. - -_Inspired by PHP's [`LogicException`](https://www.php.net/manual/en/class.logicexception)_ - -```js -import { LogicalError } from "@aedart/support/exceptions"; - -function print(person) { - if (printer === undefined) { - throw new LogicalError('Printer is missing, unable to print people'); - } -} -``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/exceptions/README.md b/docs/archive/current/packages/support/exceptions/README.md new file mode 100644 index 00000000..51cdc484 --- /dev/null +++ b/docs/archive/current/packages/support/exceptions/README.md @@ -0,0 +1,8 @@ +--- +title: About Exceptions +description: Error & Exceptions utilities. +--- + +# Exceptions + +`@aedart/support/exceptions` offers a few utilities for working with [Custom Errors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types). \ No newline at end of file diff --git a/docs/archive/current/packages/support/exceptions/configureCustomError.md b/docs/archive/current/packages/support/exceptions/configureCustomError.md new file mode 100644 index 00000000..8de90971 --- /dev/null +++ b/docs/archive/current/packages/support/exceptions/configureCustomError.md @@ -0,0 +1,32 @@ +--- +title: Configure Custom Error +description: Configuration of custom errors. +sidebarDepth: 0 +--- + +# `configureCustomError` + +Configures a custom error by automatically setting the error's `name` property to the class' constructor name. + +## Arguments + +`configureCustomError`() accepts the following arguments: + +* `error: Error` - the custom error instance +* `captureStackTrace: boolean = false` (_optional_) Captures and sets error's stack trace¹. + +¹: _See [`configureStackTrace()`](./configurestacktrace.md) for details._ + +```js +import { configureCustomError } from "@aedart/support/exceptions"; + +class MyError extends Error { + constructor(message, options) { + super(message, options); + + configureCustomError(this); + } +} +``` + +See [Mozilla's documentation on Custom Error Types](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types) for additional information. diff --git a/docs/archive/current/packages/support/exceptions/configureStackTrace.md b/docs/archive/current/packages/support/exceptions/configureStackTrace.md new file mode 100644 index 00000000..0e743c85 --- /dev/null +++ b/docs/archive/current/packages/support/exceptions/configureStackTrace.md @@ -0,0 +1,31 @@ +--- +title: Configure Stack Trace +description: Configuration error stack trace. +sidebarDepth: 0 +--- + +# `configureStackTrace` + +Captures a new stack trace and sets given Error's [`stack`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack) property. + +## Arguments + +The function accepts an `Error` as argument. + +```js +import { configureStackTrace } from "@aedart/support/exceptions"; + +class MyError extends Error { + constructor(message, options) { + super(message, options); + + configureStackTrace(this); + } +} +``` + +::: warning +The `stack` is [not yet an official feature](https://github.com/tc39/proposal-error-stacks), even though it's supported by many major browsers and Node.js. +If you are working with custom errors, you might not need to capture and set the `stack` property. +Therefore, you should only use `configureStackTrace()` in situations when your JavaScript environment does not support stack traces in custom errors. +::: \ No newline at end of file diff --git a/docs/archive/current/packages/support/exceptions/customErrors.md b/docs/archive/current/packages/support/exceptions/customErrors.md new file mode 100644 index 00000000..c156c052 --- /dev/null +++ b/docs/archive/current/packages/support/exceptions/customErrors.md @@ -0,0 +1,46 @@ +--- +title: Custom Errors +description: Predefined custom errors. +sidebarDepth: 0 +--- + +# Custom Errors + +[[TOC]] + +## `AbstractClassError` + +The `AbstractClassError` is intended to be thrown whenever an abstract class is attempted instantiated directly. + +```js +import { AbstractClassError } from "@aedart/support/exceptions"; + +/** + * @abstract + */ +class MyAbstractClass { + constructor() { + if (new.target === MyAbstractClass) { + throw new AbstractClassError(MyAbstractClass); + } + } +} + +const instance = new MyAbstractClass(); // AbstractClassError +``` + +## `LogicalError` + +To be thrown whenever there is an error in the programming logic. + +_Inspired by PHP's [`LogicException`](https://www.php.net/manual/en/class.logicexception)_ + +```js +import { LogicalError } from "@aedart/support/exceptions"; + +function print(person) { + if (printer === undefined) { + throw new LogicalError('Printer is missing, unable to print people'); + } +} +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/exceptions/getErrorMessage.md b/docs/archive/current/packages/support/exceptions/getErrorMessage.md new file mode 100644 index 00000000..9251f12f --- /dev/null +++ b/docs/archive/current/packages/support/exceptions/getErrorMessage.md @@ -0,0 +1,28 @@ +--- +title: Get Error Message +description: Obtain error or default message. +sidebarDepth: 0 +--- + +# `getErrorMessage` + +Returns an Error's `message`, if an [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) +instance is provided. Otherwise, a default message is returned. + +```js +import { getErrorMessage } from "@aedart/support/exceptions"; + +try { + throw new Error('Something went wrong!'); +} catch(e) { + const msg = getErrorMessage(e, 'unknown error'); // Something went wrong! +} + +// --------------------------------------------------------------------------- + +try { + throw 'Something went wrong!'; +} catch(e) { + const msg = getErrorMessage(e, 'unknown error'); // unknown error +} +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/meta.md b/docs/archive/current/packages/support/meta.md deleted file mode 100644 index 0d1bcc04..00000000 --- a/docs/archive/current/packages/support/meta.md +++ /dev/null @@ -1,426 +0,0 @@ ---- -title: Meta -description: Add arbitrary metadata on classes, methods and properties. -sidebarDepth: 0 ---- - -# Meta - -Provides a decorator that is able to associate metadata with a class, its methods and properties. - -```js -import {meta, getMeta} from '@aedart/support/meta'; - -@meta('service_alias', 'locationSearcher') -class Service {} - -getMeta(Service, 'service_alias'); // locationSearcher -``` - -[[TOC]] - -## Prerequisites - -At the time of this writing, [decorators](https://github.com/tc39/proposal-decorators) are still in a proposal phase. -To use the meta decorator, you must either use [@babel/plugin-proposal-decorators](https://babeljs.io/docs/babel-plugin-proposal-decorators), or use [TypeScript 5 decorators](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#decorators). - -## Supported Elements - -The `meta` decorator supports the following elements¹: - -* `class` -* `method` -* `getter` -* `setter` -* `field` -* `accessor` - -¹: _An element is determined by the decorator's [`context.kind`](https://github.com/tc39/proposal-decorators#2-calling-decorators) property._ - -## Defining and Retrieving Metadata - -To define metadata on a class or its elements, use `meta()`. -It accepts the following arguments: - -* `key`: _name of metadata identifier. Can also be a path (_see [`set`](./objects.md#set)_)._ -* `value`: _arbitrary data. Can be a [primitive value](https://developer.mozilla.org/en-US/docs/Glossary/Primitive), an [object](https://developer.mozilla.org/en-US/docs/Glossary/Object), or a [function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function)._ - -To obtain metadata, use the `getMeta()` method. -You can also use `getAllMeta()`, if you wish to obtain all available metadata for a target class. - -```js -import {meta, getMeta, getAllMeta} from '@aedart/support/meta'; - -@meta('service_alias', 'locationSearcher') -class Service -{ - @meta('name', 'Name of service') name; - - @meta('fetch.desc', 'Fetches resource via a gateway') - @meta('fetch.dependencies', [ 'my-gateway' ]) - async fetch(gateway) - { - // ...implementation not shown... - } -} - -// Later in your application... -const service = new Service(); - -const desc = getMeta(Service, 'fetch.desc'); -const dependencies = getMeta(Service, 'fetch.dependencies'); - -// Or, obtain all metadata -const allMeta = getAllMeta(Service); -``` - -::: tip Metadata Availability -Depending on the kind of element that is decorated, metadata might only **_become available_** for reading, **_after_** a new class instance has been instantiated. -This is true for the following elements: -* `method` -* `getter` -* `setter` -* `field` -* `accessor` - -**Static Elements** - -If an element is declared as [`static`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static), then it's metadata becomes available as soon as the class has been defined. -::: - -### Default Value - -The `getMeta()` method also offers a `defaultValue` argument, which is returned, in case that a metadata value does not exist for a given identifier. - -```js -const description = getMeta(Service, 'fetch.desc', 'N/A - method has no description'); -``` - -### Callback - -If you need to create more advanced metadata, you can specify a callback as the first argument for the `meta()` decorator method. -When using a callback you gain access to the `target` that is being decorated, as well as the decorator `context`. -The callback **MUST** return an object that contains a `key` and a `value` property. - -```js -import {meta} from '@aedart/support/meta'; - -class Service { - - @meta((target, context) => { - return { - key: context.name, - value: '...' - } - }) - delegateTo(gateway) { - // ...not shown... - } -} -``` - -Although the above example is a bit cumbersome to read, it shows a simple way to defined metadata for a method, which utilises the decorator `context`. -If you wish, you can use this approach to create your own specialised meta decorators. Doing so can also improve the readability of your class. -Consider the following example: - -```js -import {meta} from '@aedart/support/meta'; - -function delegateMeta() { - return meta((target, context) => { - return { - key: context.name, - value: '...' - } - }); -} - -class Service { - - @delegateMeta() - delegateTo(gateway) { - // ...not shown... - } -} -``` - -## Inheritance - -Metadata is automatically inherited by subclasses. - -```js -import {meta, getMeta} from '@aedart/support/meta'; - -@meta('service_alias', 'locationSearcher') -class Service {} - -class CitySearcher extends Service {} - -getMeta(CitySearcher, 'service_alias'); // locationSearcher -``` - -### Overwrites - -You can also overwrite the inherited metadata. The subclass that defines the metadata creates its own copy of the inherited metadata. -The parent class' metadata remains untouched. - -```js -import {meta, getMeta} from '@aedart/support/meta'; - -class Service { - - @meta('search.desc', 'Searches for countries') - search() { - // ...not shown... - } -} - -class CitySearcher extends Service { - - @meta('search.desc', 'Searches for cities') - search() { - // ...not shown... - } -} - -const service = new CitySearcher(); - -getMeta(CitySearcher, 'search.desc'); // Searches for cities -getMeta(Service, 'search.desc'); // Searches for countries -``` - -## Changes outside the decorator - -Whenever you read metadata, **_a copy_** is returned by the `getMeta()` method. -This means that you can change the data, in your given context, but the original metadata remains the same. - -```js -import {meta, getMeta} from '@aedart/support/meta'; - -@meta('description', { name: 'Search Service', alias: 'Location Sercher' }) -class Service {} - -// Obtain "copy" and change it... -let desc = getMeta(Service, 'description'); -desc.name = 'Country Searcher'; - -// Original remains unchanged -getMeta(Service, 'description').name; // Search Service -``` - -::: warning Caution -Only the `meta` decorator is intended to alter existing metadata - _even if the value is an object_. -Please be mindful of this behaviour, whenever you change retrieved metadata using the `getMeta()` and `getAllMeta()` methods. -::: - -## TC39 Decorator Metadata - -In relation to the [Decorator Metadata proposal](https://github.com/tc39/proposal-decorator-metadata), this decorator _"mimics"_ a similar behaviour as the one defined by the proposal. -Defining and retrieving metadata relies on a decorator's `context.metadata` object, and the `Symbol.metadata` property of a class. - -**Example:** - -```js -import {meta, getMeta} from '@aedart/support/meta'; - -@meta('service_alias', 'locationSearcher') -class Service {} - -getMeta(Service, 'service_alias'); // locationSearcher -``` - -**Roughly "desugars" to the following:** -```js -function meta(key, value) { - return (target, context) => { - context.metadata[key] = value; - } -} - -@meta('service_alias', 'locationSearcher') -class Service {} - -Service[Symbol.metadata].service_alias; // locationSearcher -``` -(_Above shown example is very simplified. Actual implementation is a bit more complex..._) - -At present, the internal mechanisms of the `meta` decorator must rely on a [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) to associate metadata with the intended class. -When the [Decorator Metadata proposal](https://github.com/tc39/proposal-decorator-metadata) becomes more mature and transpilers offer the `context.metadata` object (_or when browsers support it_), -then this decorator will be updated respectfully to use the available metadata object. - -## Target Meta - -The `targetMeta()` decorator offers the ability to associate metadata directly with a class instance or class method reference. -This can be useful in situations when you do not know the class that owns the metadata. - -Behind the scene, `targetMeta()` uses the `meta()` decorator and stores a reference to the target that is decorated inside a [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap). - -::: tip Supported Elements - -Unlike the [`meta()` decorator](#supported-elements), `targetMeta()` only supports the following elements: - -* `class` -* `method` - -::: - -**Example: class instance** - -```js -import {targetMeta, getTargetMeta} from '@aedart/support/meta'; - -@targetMeta('description', { type: 'Search Service', alias: 'Location Sercher' }) -class LocationSearcherService {} - -const instance = new LocationSearcherService(); - -// ...later in your application... -getTargetMeta(instance, 'description')?.type; // Search Service -``` - -**Example: method reference** - -```js -import {targetMeta, getTargetMeta} from '@aedart/support/meta'; - -class LocationSearcherService { - - @targetMeta('dependencies', [ 'httpClient' ]) - search(apiClient) {} -} - -const instance = new LocationSearcherService(); - -// ...later in your application... -getTargetMeta(instance.search, 'dependencies'); // [ 'httpClient' ] -``` - -### Inheritance - -Target meta is automatically inherited by subclasses and can also be overwritten, similar to that of the [`meta()` decorator](#inheritance). - -**Example: classes** - -```js -import {targetMeta, getTargetMeta} from '@aedart/support/meta'; - -@meta('service_alias', 'locationSearcher') -class Service {} - -class CitySearcher extends Service {} - -const instance = new CitySearcher(); - -// ...later in your application... -getTargetMeta(instance, 'service_alias'); // locationSearcher -``` - -**Example: methods** - -```js -import {targetMeta, getTargetMeta} from '@aedart/support/meta'; - -class Service { - - @targetMeta('dependencies', [ 'countrySearchApiClient' ]) - search(apiClient) { - // ...not shown... - } -} - -class CountrySearcher extends Service { - // ... not method overwrite here... -} - -class CitySearcher extends Service { - - @targetMeta('dependencies', [ 'citySearchApiClient' ]) - search(apiClient) { - // ...not shown... - } -} - -const instanceA = new Service(); -const instanceB = new CountrySearcher(); -const instanceC = new CitySearcher(); - -// ...later in your application... -getTargetMeta(instanceA.search, 'dependencies'); // [ 'countrySearchApiClient' ] -getTargetMeta(instanceB.search, 'dependencies'); // [ 'countrySearchApiClient' ] -getTargetMeta(instanceC.search, 'dependencies'); // [ 'citySearchApiClient' ] -``` - -#### Static Methods - -Inheritance for static methods works a bit differently. By default, any subclass will automatically inherit target metadata, even for static methods. -However, if you overwrite the given static method, the metadata is lost. - -::: tip Limitation - -_When a static method is overwritten, the parent's "target" metadata cannot be obtained due to a general limitation of the `meta()` decorator. -The decorator has no late `this` binding available to the overwritten static method. -This makes it impossible to associate the overwritten static method with metadata from the parent._ - -::: - -**Example: inheritance for static methods** - -```js -import {targetMeta, getTargetMeta} from '@aedart/support/meta'; - -class Service { - - @targetMeta('dependencies', [ 'xmlClient' ]) - static search(client) { - // ...not shown... - } -} - -class CountrySearcher extends Service { - // ... not method overwrite here... -} - -class CitySearcher extends Service { - - // Overwite of static method - target meta is lost - static search(client) {} -} - -// ...later in your application... -getTargetMeta(CountrySearcher.search, 'dependencies'); // [ 'xmlClient' ] -getTargetMeta(CitySearcher.search, 'dependencies'); // undefined -``` - -To overcome the above shown issue, you can use the `inheritTargetMeta()` decorator. It forces the static method to "copy" metadata from its parent, if available. - -**Example: force inheritance for static methods** - -```js -import { - targetMeta, - getTargetMeta, - inheritTargetMeta -} from '@aedart/support/meta'; - -class Service { - - @targetMeta('dependencies', [ 'xmlClient' ]) - static search(client) { - // ...not shown... - } -} - -class CountrySearcher extends Service { - // ... not method overwrite here... -} - -class CitySearcher extends Service { - - @inheritTargetMeta() - static search(client) {} -} - -// ...later in your application... -getTargetMeta(CountrySearcher.search, 'dependencies'); // [ 'xmlClient' ] -getTargetMeta(CitySearcher.search, 'dependencies'); // [ 'xmlClient' ] -``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/meta/README.md b/docs/archive/current/packages/support/meta/README.md new file mode 100644 index 00000000..dca7ad73 --- /dev/null +++ b/docs/archive/current/packages/support/meta/README.md @@ -0,0 +1,18 @@ +--- +title: About Meta +description: Add arbitrary metadata on classes, methods and properties. +--- + + +# About Meta + +Provides a decorator that is able to associate metadata with a class, its methods and properties. + +```js +import { meta, getMeta } from '@aedart/support/meta'; + +@meta('service_alias', 'locationSearcher') +class Service {} + +getMeta(Service, 'service_alias'); // locationSearcher +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/meta/inheritance.md b/docs/archive/current/packages/support/meta/inheritance.md new file mode 100644 index 00000000..591a187f --- /dev/null +++ b/docs/archive/current/packages/support/meta/inheritance.md @@ -0,0 +1,51 @@ +--- +title: Inheritance +description: About metadata inheritance and overwrites. +sidebarDepth: 0 +--- + +# Inheritance + +Metadata is automatically inherited by subclasses. + +```js +import { meta, getMeta } from '@aedart/support/meta'; + +@meta('service_alias', 'locationSearcher') +class Service {} + +class CitySearcher extends Service {} + +getMeta(CitySearcher, 'service_alias'); // locationSearcher +``` + + +## Overwrites + +You can also overwrite the inherited metadata. The subclass that defines the metadata creates its own copy of the inherited metadata. +The parent class' metadata remains untouched. + +```js +import { meta, getMeta } from '@aedart/support/meta'; + +class Service { + + @meta('search.desc', 'Searches for countries') + search() { + // ...not shown... + } +} + +class CitySearcher extends Service { + + @meta('search.desc', 'Searches for cities') + search() { + // ...not shown... + } +} + +const service = new CitySearcher(); + +getMeta(CitySearcher, 'search.desc'); // Searches for cities +getMeta(Service, 'search.desc'); // Searches for countries +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/meta/outsideChanges.md b/docs/archive/current/packages/support/meta/outsideChanges.md new file mode 100644 index 00000000..eadd59d4 --- /dev/null +++ b/docs/archive/current/packages/support/meta/outsideChanges.md @@ -0,0 +1,29 @@ +--- +title: Outside Changes +description: About changes to metadata outside decorator scope. +sidebarDepth: 0 +--- + +# Changes outside the decorator + +Whenever you read metadata, **_a copy_** is returned by the `getMeta()` method. +This means that you can change the data, in your given context, but the original metadata remains the same. + +```js +import { meta, getMeta } from '@aedart/support/meta'; + +@meta('description', { name: 'Search Service', alias: 'Location Sercher' }) +class Service {} + +// Obtain "copy" and change it... +let desc = getMeta(Service, 'description'); +desc.name = 'Country Searcher'; + +// Original remains unchanged +getMeta(Service, 'description').name; // Search Service +``` + +::: warning Caution +Only the `meta` decorator is intended to alter existing metadata - _even if the value is an object_. +Please be mindful of this behaviour, whenever you change retrieved metadata using the `getMeta()` and `getAllMeta()` methods. +::: \ No newline at end of file diff --git a/docs/archive/current/packages/support/meta/prerequisites.md b/docs/archive/current/packages/support/meta/prerequisites.md new file mode 100644 index 00000000..6e54aae8 --- /dev/null +++ b/docs/archive/current/packages/support/meta/prerequisites.md @@ -0,0 +1,10 @@ +--- +title: Prerequisites +description: Prerequisites for using meta decorators. +sidebarDepth: 0 +--- + +# Prerequisites + +At the time of this writing, [decorators](https://github.com/tc39/proposal-decorators) are still in a proposal phase. +To use the meta decorator, you must either use [@babel/plugin-proposal-decorators](https://babeljs.io/docs/babel-plugin-proposal-decorators), or use [TypeScript 5 decorators](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#decorators). \ No newline at end of file diff --git a/docs/archive/current/packages/support/meta/setAndGet.md b/docs/archive/current/packages/support/meta/setAndGet.md new file mode 100644 index 00000000..63634330 --- /dev/null +++ b/docs/archive/current/packages/support/meta/setAndGet.md @@ -0,0 +1,123 @@ +--- +title: Set & Get +description: Defining and retrieving metadata. +sidebarDepth: 0 +--- + +# Set and Get Metadata + +[[TOC]] + +## Set Metadata + +To define metadata on a class or its elements, use `meta()`. +It accepts the following arguments: + +* `key`: _name of metadata identifier. Can also be a path (_see [`set`](../objects/set.md)_)._ +* `value`: _arbitrary data. Can be a [primitive value](https://developer.mozilla.org/en-US/docs/Glossary/Primitive), an [object](https://developer.mozilla.org/en-US/docs/Glossary/Object), or a [function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function)._ + +To obtain metadata, use the `getMeta()` method. +You can also use `getAllMeta()`, if you wish to obtain all available metadata for a target class. + +```js +import { meta } from '@aedart/support/meta'; + +@meta('service_alias', 'locationSearcher') +class Service +{ + @meta('name', 'Name of service') name; + + @meta('fetch.desc', 'Fetches resource via a gateway') + @meta('fetch.dependencies', [ 'my-gateway' ]) + async fetch(gateway) + { + // ...implementation not shown... + } +} +``` + +## Get Metadata + +Use `getMeta()` or `getAllMeta()` to retrieve metadata. + +```js +import { getMeta, getAllMeta } from '@aedart/support/meta'; + +const service = new Service(); + +const desc = getMeta(Service, 'fetch.desc'); +const dependencies = getMeta(Service, 'fetch.dependencies'); + +// Or, obtain all metadata +const allMeta = getAllMeta(Service); +``` + +::: tip Metadata Availability +Depending on the kind of element that is decorated, metadata might only **_become available_** for reading, **_after_** a new class instance has been instantiated. +This is true for the following elements: +* `method` +* `getter` +* `setter` +* `field` +* `accessor` + +**Static Elements** + +If an element is declared as [`static`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static), then it's metadata becomes available as soon as the class has been defined. +::: + +### Default Value + +The `getMeta()` method also offers a `defaultValue` argument, which is returned, in case that a metadata value does not exist for a given identifier. + +```js +const description = getMeta(Service, 'fetch.desc', 'N/A'); +``` + +## Callback + +If you need to create more advanced metadata, you can specify a callback as the first argument for the `meta()` decorator method. +When using a callback you gain access to the `target` that is being decorated, as well as the decorator `context`. +The callback **MUST** return an object that contains a `key` and a `value` property. + +```js +import { meta } from '@aedart/support/meta'; + +class Service { + + @meta((target, context) => { + return { + key: context.name, + value: '...' + } + }) + delegateTo(gateway) { + // ...not shown... + } +} +``` + +Although the above example is a bit cumbersome to read, it shows a simple way to defined metadata for a method, which utilises the decorator `context`. +If you wish, you can use this approach to create your own specialised meta decorators. Doing so can also improve the readability of your class. +Consider the following example: + +```js +import { meta } from '@aedart/support/meta'; + +function delegateMeta() { + return meta((target, context) => { + return { + key: context.name, + value: '...' + } + }); +} + +class Service { + + @delegateMeta() + delegateTo(gateway) { + // ...not shown... + } +} +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/meta/supported.md b/docs/archive/current/packages/support/meta/supported.md new file mode 100644 index 00000000..65e7e580 --- /dev/null +++ b/docs/archive/current/packages/support/meta/supported.md @@ -0,0 +1,18 @@ +--- +title: Supported Elements +description: Supported elements by meta decorators. +sidebarDepth: 0 +--- + +# Supported Elements + +The `meta` decorator supports the following elements¹: + +* `class` +* `method` +* `getter` +* `setter` +* `field` +* `accessor` + +¹: _An element is determined by the decorator's [`context.kind`](https://github.com/tc39/proposal-decorators#2-calling-decorators) property._ \ No newline at end of file diff --git a/docs/archive/current/packages/support/meta/targetMeta.md b/docs/archive/current/packages/support/meta/targetMeta.md new file mode 100644 index 00000000..41ba2268 --- /dev/null +++ b/docs/archive/current/packages/support/meta/targetMeta.md @@ -0,0 +1,186 @@ +--- +title: Target Meta +description: Associate metadata with a class instance or class method reference. +sidebarDepth: 0 +--- + +# Target Meta + +The `targetMeta()` decorator offers the ability to associate metadata directly with a class instance or class method reference. +This can be useful in situations when you do not know the class that owns the metadata. + +Behind the scene, `targetMeta()` uses the `meta()` decorator and stores a reference to the target that is decorated inside a [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap). + +[[TOC]] + +## Supported Elements + +Unlike the [`meta()` decorator](./supported.md), `targetMeta()` only supports the following elements: + +* `class` +* `method` + +## Class Instance + +The following shows how to define target meta for a class and retrieve it. + +```js +import { targetMeta, getTargetMeta } from '@aedart/support/meta'; + +@targetMeta('description', { type: 'Search Service', alias: 'Location Sercher' }) +class LocationSearcherService {} + +const instance = new LocationSearcherService(); + +// ...later in your application... +getTargetMeta(instance, 'description')?.type; // Search Service +``` + +## Method Reference + +The following shows how to define target meta for a class method and retrieve it. + +```js +import { targetMeta, getTargetMeta } from '@aedart/support/meta'; + +class LocationSearcherService { + + @targetMeta('dependencies', [ 'httpClient' ]) + search(apiClient) {} +} + +const instance = new LocationSearcherService(); + +// ...later in your application... +getTargetMeta(instance.search, 'dependencies'); // [ 'httpClient' ] +``` + +## Inheritance + +Target meta is automatically inherited by subclasses and can also be overwritten, similar to that of the [`meta()` decorator](#inheritance). + +**Example: classes** + +```js +import {targetMeta, getTargetMeta} from '@aedart/support/meta'; + +@meta('service_alias', 'locationSearcher') +class Service {} + +class CitySearcher extends Service {} + +const instance = new CitySearcher(); + +// ...later in your application... +getTargetMeta(instance, 'service_alias'); // locationSearcher +``` + +**Example: methods** + +```js +import {targetMeta, getTargetMeta} from '@aedart/support/meta'; + +class Service { + + @targetMeta('dependencies', [ 'countrySearchApiClient' ]) + search(apiClient) { + // ...not shown... + } +} + +class CountrySearcher extends Service { + // ... not method overwrite here... +} + +class CitySearcher extends Service { + + @targetMeta('dependencies', [ 'citySearchApiClient' ]) + search(apiClient) { + // ...not shown... + } +} + +const instanceA = new Service(); +const instanceB = new CountrySearcher(); +const instanceC = new CitySearcher(); + +// ...later in your application... +getTargetMeta(instanceA.search, 'dependencies'); // [ 'countrySearchApiClient' ] +getTargetMeta(instanceB.search, 'dependencies'); // [ 'countrySearchApiClient' ] +getTargetMeta(instanceC.search, 'dependencies'); // [ 'citySearchApiClient' ] +``` + +### Static Methods + +Inheritance for static methods works a bit differently. By default, any subclass will automatically inherit target metadata, even for static methods. +However, if you overwrite the given static method, the metadata is lost. + +::: tip Limitation + +_When a static method is overwritten, the parent's "target" metadata cannot be obtained due to a general limitation of the `meta()` decorator. +The decorator has no late `this` binding available to the overwritten static method. +This makes it impossible to associate the overwritten static method with metadata from the parent._ + +::: + +**Example: inheritance for static methods** + +```js +import {targetMeta, getTargetMeta} from '@aedart/support/meta'; + +class Service { + + @targetMeta('dependencies', [ 'xmlClient' ]) + static search(client) { + // ...not shown... + } +} + +class CountrySearcher extends Service { + // ... not method overwrite here... +} + +class CitySearcher extends Service { + + // Overwite of static method - target meta is lost + static search(client) {} +} + +// ...later in your application... +getTargetMeta(CountrySearcher.search, 'dependencies'); // [ 'xmlClient' ] +getTargetMeta(CitySearcher.search, 'dependencies'); // undefined +``` + +To overcome the above shown issue, you can use the `inheritTargetMeta()` decorator. It forces the static method to "copy" metadata from its parent, if available. + +**Example: force inheritance for static methods** + +```js +import { + targetMeta, + getTargetMeta, + inheritTargetMeta +} from '@aedart/support/meta'; + +class Service { + + @targetMeta('dependencies', [ 'xmlClient' ]) + static search(client) { + // ...not shown... + } +} + +class CountrySearcher extends Service { + // ... not method overwrite here... +} + +class CitySearcher extends Service { + + @inheritTargetMeta() + static search(client) {} +} + +// ...later in your application... +getTargetMeta(CountrySearcher.search, 'dependencies'); // [ 'xmlClient' ] +getTargetMeta(CitySearcher.search, 'dependencies'); // [ 'xmlClient' ] +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/meta/tc39.md b/docs/archive/current/packages/support/meta/tc39.md new file mode 100644 index 00000000..fce4b579 --- /dev/null +++ b/docs/archive/current/packages/support/meta/tc39.md @@ -0,0 +1,40 @@ +--- +title: TC39 Proposal +description: In relation to TC39 Decorator Metadata proposal +sidebarDepth: 0 +--- + +# TC39 Decorator Metadata + +In relation to the [Decorator Metadata proposal](https://github.com/tc39/proposal-decorator-metadata), this decorator _"mimics"_ a similar behaviour as the one defined by the proposal. +Defining and retrieving metadata relies on a decorator's `context.metadata` object, and the `Symbol.metadata` property of a class. + +**Example:** + +```js +import { meta, getMeta } from '@aedart/support/meta'; + +@meta('service_alias', 'locationSearcher') +class Service {} + +getMeta(Service, 'service_alias'); // locationSearcher +``` + +**Roughly "desugars" to the following:** +```js +function meta(key, value) { + return (target, context) => { + context.metadata[key] = value; + } +} + +@meta('service_alias', 'locationSearcher') +class Service {} + +Service[Symbol.metadata].service_alias; // locationSearcher +``` +(_Above shown example is very simplified. Actual implementation is a bit more complex..._) + +At present, the internal mechanisms of the `meta` decorator must rely on a [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) to associate metadata with the intended class. +When the [Decorator Metadata proposal](https://github.com/tc39/proposal-decorator-metadata) becomes more mature and transpilers offer the `context.metadata` object (_or when browsers support it_), +then this decorator will be updated respectfully to use the available metadata object. \ No newline at end of file diff --git a/docs/archive/current/packages/support/misc.md b/docs/archive/current/packages/support/misc.md deleted file mode 100644 index ae09942e..00000000 --- a/docs/archive/current/packages/support/misc.md +++ /dev/null @@ -1,199 +0,0 @@ ---- -title: Misc. -description: Misc. utilities -sidebarDepth: 0 ---- - -# Misc. - -`@aedart/support/misc` offers miscellaneous utility methods. - -[[TOC]] - -## `descTag` - -Return the default string description of an object. - -```js -import {descTag} from '@aedart/support/misc'; - -descTag('foo'); // [object String] -descTag(3); // [object Number] -descTag([1, 2, 3]); // [object Array] -descTag(true); // [object Boolean] -// ... etc -``` - -The method is a shorthand for the following: - -```js -Object.prototype.toString.call(/* your value */); -``` - -See [Mozilla's documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag) for additional information. - - -## `empty` - -Determine if value is empty. - -_See also [`isset()`](#isset)._ - -```js -import {empty} from '@aedart/support/misc'; - -empty(''); // true -empty(false); // true -empty(0); // true -empty(0n); // true -empty(NaN); // true -empty(null); // true -empty(undefined); // true -empty([]); // true -empty({}); // true -empty(new Set()); // true -empty(new Map()); // true -empty(new Int8Array()); // true - -empty(' '); // false -empty('a'); // false -empty(true); // false -empty(1); // false -empty(1n); // false -empty(-1); // false -empty(Infinity); // false -empty([ 1 ]); // false -empty({ name: 'Jimmy' }); // false -empty((new Set()).add('a')); // false -empty((new Map).set('foo', 'bar')); // false -empty(new Date()); // false -empty(function() {}); // false -empty(Symbol('my-symbol')); // false - -let typedArr = new Int8Array(1); -typedArr[0] = 1; -empty(typedArr); // false -``` - -::: warning WeakMap and WeakSet -`empty()` is not able to determine if a [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) or [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) is empty. -::: - -## `isKey` - -Determine if given is a valid [key](#ispropertykey) or [property path identifier](./objects.md#has). - -```js -import {isKey} from '@aedart/support/misc'; - -isKey('foo'); // true -isKey(12); // true -isKey(Symbol('my-symbol')); // true -isKey([ 'a', 'b.c', Symbol('my-other-symbol')]); // true - -isKey(true); // false -isKey([]); // false -isKey(null); // false -isKey(undefined); // false -isKey(() => true); // false -``` - -## `isPrimitive` - -Determine if a value is a [primitive value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values). - -```js -import {isPrimitive} from '@aedart/support/misc'; - -isPrimitive(null); // true -isPrimitive(undefined); // true -isPrimitive(true); // true -isPrimitive(1); // true -isPrimitive(1n); // true -isPrimitive('foo'); // true -isPrimitive(Symbol('my-symbol')); // true - -isPrimitive([1, 2, 3]); // false -isPrimitive({ name: 'Rian' }); // false -isPrimitive(function() {}); // false -``` - -## `isPropertyKey` - -Determine if a key a valid property key name. - -```js -import {isPropertyKey} from '@aedart/support/misc'; - -isPropertyKey('foo'); // true -isPropertyKey(12); // true -isPropertyKey(Symbol('my-symbol')); // true - -isPropertyKey(true); // false -isPropertyKey(['a', 'b', 'c']); // false -isPropertyKey(null); // false -isPropertyKey(undefined); // false -isPropertyKey(() => true); // false -``` - -## `isset` - -Determine if value is different from `undefined` and `null`. - -_See also [`empty()`](#empty)._ - -```js -import {isset} from '@aedart/support/misc'; - -isset('foo'); // true -isset(''); // true -isset(true); // true -isset(false); // true -isset(1234); // true -isset(1.234); // true -isset([]); // true -isset({}); // true -isset(() => true); // true - -isset(undefined); // false -isset(null); // false -``` - -You can also determine if multiple values differ from `undefined` and `null`. - -**Note**: _All given values must differ from `undefined` and `null`, before method returns `true`._ - -```js -isset('foo', { name: 'Jane' }, [ 1, 2, 3 ]); // true - -isset('foo', null, [ 1, 2, 3 ]); // false -isset('foo', { name: 'Jane' }, undefined); // false -``` - -## `mergeKeys` - -The `mergeKeys()` method is able to merge two or more keys into a single key (_see [`isKey()`](#iskey)_). - -```js -import { mergeKeys } from "@aedart/support/misc"; - -const key = mergeKeys(Symbol('my-symbol'), [ 'b', 'c.d' ], 23); - -console.log(key); // [ Symbol('my-symbol'), 'b', 'c.d', 23 ]; -``` - -## `toWeakRef` - -Wraps a target object into a [`WeakRef`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef), if not already instance of a weak reference. - -```js -import { toWeakRef } from "@aedart/support/misc"; - -const person = { name: 'Sine' }; - -const a = toWeakRef(person); // new WeakRef of "person" -const b = toWeakRef(a); // same WeakRef instance as "a" - -toWeakRef(null); // undefined -toWeakRef(undefined); // undefined -``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/misc/README.md b/docs/archive/current/packages/support/misc/README.md new file mode 100644 index 00000000..d834dd6f --- /dev/null +++ b/docs/archive/current/packages/support/misc/README.md @@ -0,0 +1,8 @@ +--- +title: About Misc. +description: Miscellaneous utility functions. +--- + +# About Misc. + +`@aedart/support/misc` offers miscellaneous utility functions. \ No newline at end of file diff --git a/docs/archive/current/packages/support/misc/descTag.md b/docs/archive/current/packages/support/misc/descTag.md new file mode 100644 index 00000000..d7d87668 --- /dev/null +++ b/docs/archive/current/packages/support/misc/descTag.md @@ -0,0 +1,28 @@ +--- +title: Desc. Tag +description: Object description tag +sidebarDepth: 0 +--- + +# `descTag` + +Return the default string description of an object. + +```js +import { descTag } from '@aedart/support/misc'; + +descTag('foo'); // [object String] +descTag(3); // [object Number] +descTag([1, 2, 3]); // [object Array] +descTag(true); // [object Boolean] +// ... etc +``` + +The method is a shorthand for the following: + +```js +Object.prototype.toString.call(/* your value */); +``` + +See [Mozilla's documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag) for additional information. + diff --git a/docs/archive/current/packages/support/misc/empty.md b/docs/archive/current/packages/support/misc/empty.md new file mode 100644 index 00000000..1707b7ec --- /dev/null +++ b/docs/archive/current/packages/support/misc/empty.md @@ -0,0 +1,53 @@ +--- +title: Empty +description: Determine if value is empty +sidebarDepth: 0 +--- + +# `empty` + +Determine if value is empty. + +_See also [`isset()`](./isset.md)._ + +```js +import { empty } from '@aedart/support/misc'; + +empty(''); // true +empty(false); // true +empty(0); // true +empty(0n); // true +empty(NaN); // true +empty(null); // true +empty(undefined); // true +empty([]); // true +empty({}); // true +empty(new Set()); // true +empty(new Map()); // true +empty(new Int8Array()); // true + +empty(' '); // false +empty('a'); // false +empty(true); // false +empty(1); // false +empty(1n); // false +empty(-1); // false +empty(Infinity); // false +empty([ 1 ]); // false +empty({ name: 'Jimmy' }); // false +empty((new Set()).add('a')); // false +empty((new Map).set('foo', 'bar')); // false +empty(new Date()); // false +empty(function() {}); // false +empty(Symbol('my-symbol')); // false + +let typedArr = new Int8Array(1); +typedArr[0] = 1; +empty(typedArr); // false +``` + +## WeakMap and WeakSet + +::: warning Caution +`empty()` is not able to determine if a [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) or [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) is empty. +::: diff --git a/docs/archive/current/packages/support/misc/isKey.md b/docs/archive/current/packages/support/misc/isKey.md new file mode 100644 index 00000000..3150b865 --- /dev/null +++ b/docs/archive/current/packages/support/misc/isKey.md @@ -0,0 +1,24 @@ +--- +title: Is Key +description: Determine if is a key or path identifier +sidebarDepth: 0 +--- + +# `isKey` + +Determine if given is a valid [key](./isPropertyKey.md) or [property path identifier](../objects/has.md). + +```js +import { isKey } from '@aedart/support/misc'; + +isKey('foo'); // true +isKey(12); // true +isKey(Symbol('my-symbol')); // true +isKey([ 'a', 'b.c', Symbol('my-other-symbol')]); // true + +isKey(true); // false +isKey([]); // false +isKey(null); // false +isKey(undefined); // false +isKey(() => true); // false +``` diff --git a/docs/archive/current/packages/support/misc/isPrimitive.md b/docs/archive/current/packages/support/misc/isPrimitive.md new file mode 100644 index 00000000..772cdcdc --- /dev/null +++ b/docs/archive/current/packages/support/misc/isPrimitive.md @@ -0,0 +1,25 @@ +--- +title: Is Primitive +description: Determine if value is a primitive. +sidebarDepth: 0 +--- + +# `isPrimitive` + +Determine if a value is a [primitive value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values). + +```js +import { isPrimitive } from '@aedart/support/misc'; + +isPrimitive(null); // true +isPrimitive(undefined); // true +isPrimitive(true); // true +isPrimitive(1); // true +isPrimitive(1n); // true +isPrimitive('foo'); // true +isPrimitive(Symbol('my-symbol')); // true + +isPrimitive([1, 2, 3]); // false +isPrimitive({ name: 'Rian' }); // false +isPrimitive(function() {}); // false +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/misc/isPropertyKey.md b/docs/archive/current/packages/support/misc/isPropertyKey.md new file mode 100644 index 00000000..094584b7 --- /dev/null +++ b/docs/archive/current/packages/support/misc/isPropertyKey.md @@ -0,0 +1,23 @@ +--- +title: Is Property Key +description: Determine if key is a valid property key name. +sidebarDepth: 0 +--- + +# `isPropertyKey` + +Determine if a key a valid property key name (_string, number, or symbol_). + +```js +import { isPropertyKey } from '@aedart/support/misc'; + +isPropertyKey('foo'); // true +isPropertyKey(12); // true +isPropertyKey(Symbol('my-symbol')); // true + +isPropertyKey(true); // false +isPropertyKey(['a', 'b', 'c']); // false +isPropertyKey(null); // false +isPropertyKey(undefined); // false +isPropertyKey(() => true); // false +``` diff --git a/docs/archive/current/packages/support/misc/isset.md b/docs/archive/current/packages/support/misc/isset.md new file mode 100644 index 00000000..eadaa16d --- /dev/null +++ b/docs/archive/current/packages/support/misc/isset.md @@ -0,0 +1,41 @@ +--- +title: Isset +description: Determine if value isset. +sidebarDepth: 0 +--- + +# `isset` + +Determine if value is different from `undefined` and `null`. + +_See also [`empty()`](./empty.md)._ + +```js +import { isset } from '@aedart/support/misc'; + +isset('foo'); // true +isset(''); // true +isset(true); // true +isset(false); // true +isset(1234); // true +isset(1.234); // true +isset([]); // true +isset({}); // true +isset(() => true); // true + +isset(undefined); // false +isset(null); // false +``` + +## Multiple values + +You can also determine if multiple values differ from `undefined` and `null`. + +**Note**: _All given values must differ from `undefined` and `null`, before method returns `true`._ + +```js +isset('foo', { name: 'Jane' }, [ 1, 2, 3 ]); // true + +isset('foo', null, [ 1, 2, 3 ]); // false +isset('foo', { name: 'Jane' }, undefined); // false +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/misc/mergeKeys.md b/docs/archive/current/packages/support/misc/mergeKeys.md new file mode 100644 index 00000000..96a3a798 --- /dev/null +++ b/docs/archive/current/packages/support/misc/mergeKeys.md @@ -0,0 +1,17 @@ +--- +title: Merge Keys +description: Merge keys into a single key. +sidebarDepth: 0 +--- + +# `mergeKeys` + +The `mergeKeys()` method is able to merge two or more keys into a single key (_see [`isKey()`](./isKey.md)_). + +```js +import { mergeKeys } from "@aedart/support/misc"; + +const key = mergeKeys(Symbol('my-symbol'), [ 'b', 'c.d' ], 23); + +console.log(key); // [ Symbol('my-symbol'), 'b', 'c.d', 23 ]; +``` diff --git a/docs/archive/current/packages/support/misc/toWeakRef.md b/docs/archive/current/packages/support/misc/toWeakRef.md new file mode 100644 index 00000000..8f05af1e --- /dev/null +++ b/docs/archive/current/packages/support/misc/toWeakRef.md @@ -0,0 +1,21 @@ +--- +title: To Weak Ref. +description: Wrap object into a Weak Reference. +sidebarDepth: 0 +--- + +# `toWeakRef` + +Wraps a target object into a [`WeakRef`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef), if not already instance of a weak reference. + +```js +import { toWeakRef } from "@aedart/support/misc"; + +const person = { name: 'Sine' }; + +const a = toWeakRef(person); // new WeakRef of "person" +const b = toWeakRef(a); // same WeakRef instance as "a" + +toWeakRef(null); // undefined +toWeakRef(undefined); // undefined +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/mixins.md b/docs/archive/current/packages/support/mixins.md deleted file mode 100644 index 53799188..00000000 --- a/docs/archive/current/packages/support/mixins.md +++ /dev/null @@ -1,234 +0,0 @@ ---- -title: Mixins -description: Abstract subclasses ("Mixins") utilities -sidebarDepth: 0 ---- - -# Mixins - -`@aedart/support/mixins` offers an adaptation of [Justin Fagnani's](https://justinfagnani.com/author/justinfagnani/) -[`mixwith.js`](https://github.com/justinfagnani/mixwith.js) package (_originally licensed under [Apache License 2.0](https://github.com/justinfagnani/mixwith.js?tab=Apache-2.0-1-ov-file#readme)_). - -```js -import { mix, Mixin } from "@aedart/support/mixins"; - -// Define mixin -const NameMixin = Mixin((superclass) => class extends superclass { - - #name; - - set name(value) { - this.#name = value; - } - - get name() { - return this.#name; - } -}); - -// Apply mixin... -class Item extends mix().with( - NameMixin -) { - // ...not shown... -} - -// ...Later in your application -const item = new Item(); -item.name = 'My Item'; - -console.log(item.name); // My Item -``` - -[[TOC]] - -## Define Mixin - -You can use the `Mixin` decorator to define a new mixin. -Amongst other things, the decorator will enable support for [`instanceof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof) checks. -See [`instanceof` Operator](#instanceof-operator) for additional information. - -```js -import { Mixin } from "@aedart/support/mixins"; - -export const RectangleMixin = Mixin((superclass) => class extends superclass { - length = 0 - width = 0; - - area() { - return this.length * this.width; - } -}); -``` - -### Constructor - -If you need to perform initialisation logic in your mixins, then you can do so by implementing a class [`constructor`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor). -When doing so, it is important to invoke the parent constructor via [`super()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super) and pass on eventual arguments. - -```js -import { Mixin } from "@aedart/support/mixins"; - -export const RectangleMixin = Mixin((superclass) => class extends superclass { - - constructor(...args) { - super(...args); // Invoke parent constructor and pass on arugments! - - // Perform your initialisaiton logic... - } - - // ...remaining not shown... -}); -``` - -## Applying Mixins - -To apply one or more mixins, use the `mix()` function and call `width()` with the mixins you wish to apply to a superclass. - -```js -import { mix } from "@aedart/support/mixins"; -import { - RectangleMixin, - DescMixin -} from "@acme/mixins"; - -class Box extends mix().with( - RectangleMixin, - DescMixin -) { - // ...remaining not shown... -} -``` - -### Extending Superclass - -To extend a superclass and apply mixins onto it, pass the superclass as argument for the `mix()` function. - -```js -class Shape { - // ...not shown... -} - -class Box extends mix(Shape).with( - RectangleMixin, - DescMixin -) { - // ...remaining not shown... -} -``` - -::: tip Note -By default, if you do not provide `mix()` with a superclass, an empty class is automatically created. -It is the equivalent of the following: - -```js -class Box extends mix(class {}).with( - MyMixinA, - MyMixinB, - MyMixinC, -) { - // ... -} -``` -::: - -## `instanceof` Operator - -When you defined your mixins using the [`Mixin()` decorator function](#define-mixin), then it will support `instanceof` checks. -Consider the following example: - -```js -// A regular mixin without "Mixin" decorator -const MixinA = (superclass) => class extends superclas { - // ...not shown... -}; - -// Mixin with "Mixin" decorator -const MixinB = Mixin((superclass) => class extends superclass { - // ...not shown... -}); - -// -------------------------------------------------------------------- // - -class A {} - -class B extends mix(A).with( - MixinA, - MixinB -) {} - -// -------------------------------------------------------------------- // - -const instance = new B(); - -console.log(instance instanceof A); // true -console.log(instance instanceof B); // true -console.log(instance instanceof MixinA); // false -console.log(instance instanceof MixinB); // true -``` - -## How inheritance works - -To gain an overview of how inheritance works when applying mixins onto a superclass, consider the following example: - -```js -const MyMixin = Mixin((superclass) => class extends superclass { - constructor(...args) { - super(...args); // Invokes A's constructor - } - - // Overwrites A's foo() method - foo() { - return 'zam'; - } - - // Overwrites A's bar() method - bar() { - return super.bar(); // Invoke A's bar() method - } -}); - -// -------------------------------------------------------------------- // - -class A { - foo() { - return 'foo'; - } - - bar() { - return 'bar'; - } -} - -// -------------------------------------------------------------------- // - -class B extends mix(A).with( - MyMixin -) { - constructor(...args) { - super(...args); // Invokes MyMixin's constructor - } - - // Overwrite MyMixin's foo() - foo() { - const msg = super.foo(); // Invoke MyMixin's bar() method - - return `<${msg}>`; - } -} - -// -------------------------------------------------------------------- // - -const instance = new B(); - -console.log(instance.foo()); // -console.log(instance.bar()); // bar -``` - -## Onward - -For more information and examples, please read Mozilla's documentation about [_"Mix-ins"_](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends#mix-ins), -and Justin Fagnani's blog posts: - -* [_"Real" Mixins with JavaScript Classes_](https://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/) -* [_Enhancing Mixins with Decorator Functions_](https://justinfagnani.com/2016/01/07/enhancing-mixins-with-decorator-functions/) \ No newline at end of file diff --git a/docs/archive/current/packages/support/mixins/README.md b/docs/archive/current/packages/support/mixins/README.md new file mode 100644 index 00000000..9d3892fa --- /dev/null +++ b/docs/archive/current/packages/support/mixins/README.md @@ -0,0 +1,41 @@ +--- +title: About Mixins +description: Abstract subclasses ("Mixins") utilities +sidebarDepth: 0 +--- + +# Mixins + +`@aedart/support/mixins` offers an adaptation of [Justin Fagnani's](https://justinfagnani.com/author/justinfagnani/) +[`mixwith.js`](https://github.com/justinfagnani/mixwith.js) package (_originally licensed under [Apache License 2.0](https://github.com/justinfagnani/mixwith.js?tab=Apache-2.0-1-ov-file#readme)_). + +```js +import { mix, Mixin } from "@aedart/support/mixins"; + +// Define mixin +const NameMixin = Mixin((superclass) => class extends superclass { + + #name; + + set name(value) { + this.#name = value; + } + + get name() { + return this.#name; + } +}); + +// Apply mixin... +class Item extends mix().with( + NameMixin +) { + // ...not shown... +} + +// ...Later in your application +const item = new Item(); +item.name = 'My Item'; + +console.log(item.name); // My Item +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/mixins/apply.md b/docs/archive/current/packages/support/mixins/apply.md new file mode 100644 index 00000000..74af6d03 --- /dev/null +++ b/docs/archive/current/packages/support/mixins/apply.md @@ -0,0 +1,56 @@ +--- +title: Apply Mixins +description: How to apply mixins. +sidebarDepth: 0 +--- + +# Applying Mixins + +To apply one or more mixins, use the `mix()` function and call `width()` with the mixins you wish to apply to a superclass. + +```js +import { mix } from "@aedart/support/mixins"; +import { + RectangleMixin, + DescMixin +} from "@acme/mixins"; + +class Box extends mix().with( + RectangleMixin, + DescMixin +) { + // ...remaining not shown... +} +``` + +## Extending Superclass + +To extend a superclass and apply mixins onto it, pass the superclass as argument for the `mix()` function. + +```js +class Shape { + // ...not shown... +} + +class Box extends mix(Shape).with( + RectangleMixin, + DescMixin +) { + // ...remaining not shown... +} +``` + +::: tip Note +By default, if you do not provide `mix()` with a superclass, an empty class is automatically created. +It is the equivalent of the following: + +```js +class Box extends mix(class {}).with( + MyMixinA, + MyMixinB, + MyMixinC, +) { + // ... +} +``` +::: \ No newline at end of file diff --git a/docs/archive/current/packages/support/mixins/inheritance.md b/docs/archive/current/packages/support/mixins/inheritance.md new file mode 100644 index 00000000..b39eb4f2 --- /dev/null +++ b/docs/archive/current/packages/support/mixins/inheritance.md @@ -0,0 +1,63 @@ +--- +title: Inheritance +description: How inheritance works. +sidebarDepth: 0 +--- + +# How inheritance works + +To gain an overview of how inheritance works when applying mixins onto a superclass, consider the following example: + +```js +const MyMixin = Mixin((superclass) => class extends superclass { + constructor(...args) { + super(...args); // Invokes A's constructor + } + + // Overwrites A's foo() method + foo() { + return 'zam'; + } + + // Overwrites A's bar() method + bar() { + return super.bar(); // Invoke A's bar() method + } +}); + +// -------------------------------------------------------------------- // + +class A { + foo() { + return 'foo'; + } + + bar() { + return 'bar'; + } +} + +// -------------------------------------------------------------------- // + +class B extends mix(A).with( + MyMixin +) { + constructor(...args) { + super(...args); // Invokes MyMixin's constructor + } + + // Overwrite MyMixin's foo() + foo() { + const msg = super.foo(); // Invoke MyMixin's bar() method + + return `<${msg}>`; + } +} + +// -------------------------------------------------------------------- // + +const instance = new B(); + +console.log(instance.foo()); // +console.log(instance.bar()); // bar +``` diff --git a/docs/archive/current/packages/support/mixins/instanceof.md b/docs/archive/current/packages/support/mixins/instanceof.md new file mode 100644 index 00000000..132fb6dc --- /dev/null +++ b/docs/archive/current/packages/support/mixins/instanceof.md @@ -0,0 +1,40 @@ +--- +title: Instanceof +description: Using instanceof operator. +sidebarDepth: 0 +--- + +# `instanceof` Operator + +When you defined your mixins using the [`Mixin()` decorator function](./newMixin.md), then it will support `instanceof` checks. +Consider the following example: + +```js +// A regular mixin without "Mixin" decorator +const MixinA = (superclass) => class extends superclas { + // ...not shown... +}; + +// Mixin with "Mixin" decorator +const MixinB = Mixin((superclass) => class extends superclass { + // ...not shown... +}); + +// -------------------------------------------------------------------- // + +class A {} + +class B extends mix(A).with( + MixinA, + MixinB +) {} + +// -------------------------------------------------------------------- // + +const instance = new B(); + +console.log(instance instanceof A); // true +console.log(instance instanceof B); // true +console.log(instance instanceof MixinA); // false +console.log(instance instanceof MixinB); // true +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/mixins/newMixin.md b/docs/archive/current/packages/support/mixins/newMixin.md new file mode 100644 index 00000000..3a9b4dbe --- /dev/null +++ b/docs/archive/current/packages/support/mixins/newMixin.md @@ -0,0 +1,44 @@ +--- +title: New Mixin +description: How to defined a new Mixin class. +sidebarDepth: 0 +--- + +# Define a new Mixin + +You can use the `Mixin` decorator to define a new mixin. +Amongst other things, the decorator will enable support for [`instanceof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof) checks. +See [`instanceof` Operator](./instanceof.md) for additional information. + +```js +import { Mixin } from "@aedart/support/mixins"; + +export const RectangleMixin = Mixin((superclass) => class extends superclass { + length = 0 + width = 0; + + area() { + return this.length * this.width; + } +}); +``` + +## Constructor + +If you need to perform initialisation logic in your mixins, then you can do so by implementing a class [`constructor`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor). +When doing so, it is important to invoke the parent constructor via [`super()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/super) and pass on eventual arguments. + +```js +import { Mixin } from "@aedart/support/mixins"; + +export const RectangleMixin = Mixin((superclass) => class extends superclass { + + constructor(...args) { + super(...args); // Invoke parent constructor and pass on arugments! + + // Perform your initialisaiton logic... + } + + // ...remaining not shown... +}); +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/mixins/onward.md b/docs/archive/current/packages/support/mixins/onward.md new file mode 100644 index 00000000..af555cfc --- /dev/null +++ b/docs/archive/current/packages/support/mixins/onward.md @@ -0,0 +1,13 @@ +--- +title: Onward +description: Where to read more about mixins. +sidebarDepth: 0 +--- + +# Onward + +For more information and examples, please read Mozilla's documentation about [_"Mix-ins"_](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends#mix-ins), +and Justin Fagnani's blog posts: + +* [_"Real" Mixins with JavaScript Classes_](https://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/) +* [_Enhancing Mixins with Decorator Functions_](https://justinfagnani.com/2016/01/07/enhancing-mixins-with-decorator-functions/) \ No newline at end of file diff --git a/docs/archive/current/packages/support/objects.md b/docs/archive/current/packages/support/objects.md deleted file mode 100644 index aa66222b..00000000 --- a/docs/archive/current/packages/support/objects.md +++ /dev/null @@ -1,438 +0,0 @@ ---- -title: Objects -description: Objects related utilities -sidebarDepth: 0 ---- - -# Object Utility Methods - -The `@aedart/support/objects` submodule offers object related utilities. - -[[TOC]] - -## `forget` - -Remove (_delete_) a value in object at given path. -_Method is an alias for [Lodash `unset`](https://lodash.com/docs/4.17.15#unset)._ - -```js -import {forget} from "@aedart/support/objects"; - -const target = { - a: 1234, - b: { - c: { - age: 24 - } - }, -}; - -forget(target, 'b.c'); - -console.log(target); // { a: 1234, b: {} } -``` - -## `forgetAll` - -Remove (_deletes_) all values in object, at given paths. - -```js -import {forgetAll} from "@aedart/support/objects"; - -const target = { - a: 1234, - b: { - c: { - age: 24 - } - }, -}; - -forgetAll(target, [ 'a', 'b.c.age' ]); - -console.log(target); // { b: { c: {} } } -``` - -## `get` - -Get value in object at given path. -_Method is an alias for [Lodash `get`](https://lodash.com/docs/4.17.15#get)._ - -_See also [`set()`](#set)._ - -```js -import {get} from "@aedart/support/objects"; - -const target = { - a: 1234, - b: { - c: { - age: 24 - } - }, -}; - -let age = get(target, 'b.c.age'); -console.log(age); // 24 -``` - -You can also specify a default value to be returned, if the resolved value is `undefined`. - -```js -const target = { - a: 1234, - b: { - c: { - age: undefined - } - }, -}; - -// Returns default value... -let age = get(target, 'b.c.age', 20); -console.log(age); // 20 -``` - -## `has` - -Determine if path is a property of given object. -_Method is an alias for [Lodash `hasIn`](https://lodash.com/docs/4.17.15#hasIn)._ - -_See also [`isset()`](#isset)._ - -```js -import {has} from "@aedart/support/objects"; - -const target = { - a: 1234, - b: { - c: { - age: 24 - } - }, -}; - -let result = has(target, 'b.c.age'); -console.log(result); // true -``` - -## `hasAll` - -Determine if all paths are properties of given object. - -_See also [`isset()`](#isset)._ - -```js -import {hasAll} from "@aedart/support/objects"; - -const mySymbol = Symbol('my-symbol'); -const target = { - a: 1234, - b: { - name: 'Sven', - c: { - age: 24, - [mySymbol]: true - } - }, - d: [ - { name: 'Jane'}, - { name: 'Ashley'}, - ], -}; - -const paths = [ - 'a', - 'b.name', - 'b.c.age', - ['b', 'c', mySymbol], - 'd[0]', - 'd[1].name', -]; - -let result = hasAll(target, paths); -console.log(result); // true -``` - -## `hasAny` - -Determine if any paths are properties of given object. - -```js -import {hasAny} from "@aedart/support/objects"; - -const target = { - a: 1234, - b: { - name: 'Sven', - c: { - age: 24 - } - } -}; - -const paths = [ - 'z', // does not exist - 'b.c.name', // does not exist - 'b.c.age', // exist -]; - -let result = hasAny(target, paths); -console.log(result); // true -``` - -## `hasUniqueId` - -Determine if an object has a unique id. - -_See [`uniqueId`](#uniqueid) for additional details._ - -```js -import {hasUniqueId} from "@aedart/support/objects"; - -const target = { - name: 'Ursula' -}; - -console.log(hasUniqueId(target)); // false -``` - -## `isCloneable` - -Determines if given object is "cloneable". -In this context "cloneable" means that an object implements the `Cloneable` interface, and offers a `clone()` method. - -_See `@aedart/constracts/support/objects/Cloneable` for details._ - -```js -import { isCloneable } from "@aedart/support/objects"; - -class A {}; - -class B { - clone() { - return new this(); - } -} - -isCloneable(null); // false -isCloneable([]); // false -isCloneable({}); // false -isCloneable(new A()); // false -isCloneable(new B()); // true -``` - -## `isPopulatable` - -Determines if given object is "populatable". -Here, "populatable" means that an object implements the `Populatable` interface, and offers a `populate()` method. - -_See `@aedart/constracts/support/objects/Populatable` for details._ - -```js -import { isPopulatable } from "@aedart/support/objects"; - -class A {}; - -class B { - populate(data) { - // ...not shown here... - - return this; - } -} - -isPopulatable(null); // false -isPopulatable([]); // false -isPopulatable({}); // false -isPopulatable(new A()); // false -isPopulatable(new B()); // true -``` - -## `isset` - -Determine if paths are properties of given object and have values. -This method differs from [`has()`](#has), in that it only returns true if properties' values are not `undefined` and not `null`. - -_See also [misc. `isset()`](./misc/README.md#isset)._ - -```js -import {isset} from "@aedart/support/objects"; - -const target = { - a: 1234, - b: { - name: undefined, - c: { - age: null - } - }, -}; - -console.log(isset(target, 'a')); // true -console.log(isset(target, 'b')); // true -console.log(isset(target, 'b.name')); // false -console.log(isset(target, 'b.c')); // true -console.log(isset(target, 'b.c.age')); // false -``` - -You can also check if multiple paths are set. - -```js -console.log(isset(target, 'a', 'b')); // true -console.log(isset(target, 'b.c', 'b.name')); // false -console.log(isset(target, 'a', 'b.name', 'b.c.age')); // false -``` - -## `populate` - -The `populate()` allows you to populate a target object's properties with those from a source object. -The values are [shallow copied](https://developer.mozilla.org/en-US/docs/Glossary/Shallow_copy). -It accepts the following arguments: - -* `target: object` - -* `source: object` - -* `keys: PropertyKey | PropertyKey[] | SourceKeysCallback = '*'` - The keys to select and copy from `source` object. If wildcard (`*`) given, then all properties from the `source` are selected. - If a callback is given, then that callback must return key or keys to select from `source`. - -* `safe: boolean = true` - When `true`, properties must exist in target (_must be defined in target_), before they are shallow copied. - -::: warning Caution -The `target` object is mutated by this function. -::: - -::: tip Note -["Unsafe" properties](./reflections.md#iskeyunsafe) are disregarded, regardless of what `keys` are given. -::: - -```js -import { populate } from "@aedart/support/objects"; - -class Person { - name = null; - age = null; - - constructor(data) { - populate(this, data); - } -} - -const instance = new Person({ name: 'Janine', age: 36 }); -instance.name // Janine -instance.age // 36 -``` - -### Limit keys to populate - -By default, all keys (_`*`_) from the `source` object are attempted populated into the `target`. -You can limit what properties can be populated, by specifying what keys are allowed to be populated. - -```js -class Person { - name = null; - age = null; - phone = null; - - constructor(data) { - populate(this, data, [ 'name', 'age' ]); - } -} - -const instance = new Person({ name: 'Janine', age: 36, phone: '555 555 555' }); -instance.name // Janine -instance.age // 36 -instance.phone // null -``` - -### Source Keys Callback - -If you need a more advanced way to determine what keys to populate, then you can specify a callback as the `keys` argument. - -```js -populate(target, source, (source, target) => { - if (Reflect.has(source, 'phone') && Reflect.has(target, 'phone')) { - return [ 'name', 'age', 'phone' ]; - } - - return [ 'name', 'age' ]; -}); -``` - -### When keys do not exist - -When the `safe` argument is set to `true` (_default behavior_), and a property key does not exist in the `target` object, -then a `TypeError` is thrown. - -```js -class Person { - name = null; - age = null; - - constructor(data) { - populate(this, data, [ 'name', 'age', 'phone' ]); - } -} - -const instance = new Person({ - name: 'Janine', - age: 36, - phone: '555 555 555' -}); // TypeError - phone does not exist in target -``` - -However, if a requested key does not exist in the source object, then a `TypeError` is thrown regardless of the `safe` argument value. - -```js -class Person { - name = null; - age = null; - - constructor(data) { - populate(this, data, [ 'name', 'age', 'phone' ], false); - } -} - -const instance = new Person({ - name: 'Janine', - age: 36 -}); // TypeError - phone does not exist in source -``` - -## `set` - -Set a value in object at given path. -_Method is an alias for [Lodash `set`](https://lodash.com/docs/4.17.15#set)._ - -```js -import {set} from "@aedart/support/objects"; - -const target = {}; - -set(target, 'a.foo', 'bar'); - -console.log(target); // { a: { foo: 'bar } } -``` - -## `uniqueId` - -The `uniqueId()` is able to return a "unique¹" reference identifier for any given object. - -```js -import {uniqueId, hasUniqueId} from "@aedart/support/objects"; - -const target = { - name: 'Ursula' -}; - -console.log(uniqueId(target)); // 27 - -// ...later in your application -console.log(hasUniqueId(target)); // true -console.log(uniqueId(target)); // 27 -``` - -The source code is heavily inspired by [Nicolas Gehlert's](https://github.com/ngehlert) blog post: ["_Get object reference IDs in JavaScript/TypeScript_" (September 28, 2022)](https://developapa.com/object-ids/) - -¹: _In this context, the returned number is unique in the current session. The number will NOT be unique across multiple sessions, nor guarantee that an object will receive the exact same identifier as in a previous session!_ \ No newline at end of file diff --git a/docs/archive/current/packages/support/objects/README.md b/docs/archive/current/packages/support/objects/README.md new file mode 100644 index 00000000..cfecc3a7 --- /dev/null +++ b/docs/archive/current/packages/support/objects/README.md @@ -0,0 +1,8 @@ +--- +title: About Objects +description: Objects related utilities +--- + +# About Objects + +The `@aedart/support/objects` submodule offers object related utilities. \ No newline at end of file diff --git a/docs/archive/current/packages/support/objects/forget.md b/docs/archive/current/packages/support/objects/forget.md new file mode 100644 index 00000000..09a56233 --- /dev/null +++ b/docs/archive/current/packages/support/objects/forget.md @@ -0,0 +1,27 @@ +--- +title: Forget +description: Forget object key +sidebarDepth: 0 +--- + +# `forget` + +Remove (_delete_) a value in object at given path. +_Method is an alias for [Lodash `unset`](https://lodash.com/docs/4.17.15#unset)._ + +```js +import { forget } from "@aedart/support/objects"; + +const target = { + a: 1234, + b: { + c: { + age: 24 + } + }, +}; + +forget(target, 'b.c'); + +console.log(target); // { a: 1234, b: {} } +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/objects/forgetAll.md b/docs/archive/current/packages/support/objects/forgetAll.md new file mode 100644 index 00000000..2cad2e64 --- /dev/null +++ b/docs/archive/current/packages/support/objects/forgetAll.md @@ -0,0 +1,26 @@ +--- +title: Forget All +description: Forget object all keys +sidebarDepth: 0 +--- + +# `forgetAll` + +Remove (_deletes_) all values in object, at given paths. + +```js +import { forgetAll } from "@aedart/support/objects"; + +const target = { + a: 1234, + b: { + c: { + age: 24 + } + }, +}; + +forgetAll(target, [ 'a', 'b.c.age' ]); + +console.log(target); // { b: { c: {} } } +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/objects/get.md b/docs/archive/current/packages/support/objects/get.md new file mode 100644 index 00000000..7543d2d6 --- /dev/null +++ b/docs/archive/current/packages/support/objects/get.md @@ -0,0 +1,47 @@ +--- +title: Get +description: Get object value from given path. +sidebarDepth: 0 +--- + +# `get` + +Get value in object at given path. +_Method is an alias for [Lodash `get`](https://lodash.com/docs/4.17.15#get)._ + +_See also [`set()`](./set.md)._ + +```js +import { get } from "@aedart/support/objects"; + +const target = { + a: 1234, + b: { + c: { + age: 24 + } + }, +}; + +let age = get(target, 'b.c.age'); +console.log(age); // 24 +``` + +## Default Value + +You can also specify a default value to be returned, if the resolved value is `undefined`. + +```js +const target = { + a: 1234, + b: { + c: { + age: undefined + } + }, +}; + +// Returns default value... +let age = get(target, 'b.c.age', 20); +console.log(age); // 20 +``` diff --git a/docs/archive/current/packages/support/objects/has.md b/docs/archive/current/packages/support/objects/has.md new file mode 100644 index 00000000..3af13231 --- /dev/null +++ b/docs/archive/current/packages/support/objects/has.md @@ -0,0 +1,28 @@ +--- +title: Has +description: Determine if object path is a property. +sidebarDepth: 0 +--- + +# `has` + +Determine if path is a property of given object. +_Method is an alias for [Lodash `hasIn`](https://lodash.com/docs/4.17.15#hasIn)._ + +_See also [`isset()`](./isset.md)._ + +```js +import { has } from "@aedart/support/objects"; + +const target = { + a: 1234, + b: { + c: { + age: 24 + } + }, +}; + +let result = has(target, 'b.c.age'); +console.log(result); // true +``` diff --git a/docs/archive/current/packages/support/objects/hasAll.md b/docs/archive/current/packages/support/objects/hasAll.md new file mode 100644 index 00000000..08c0a746 --- /dev/null +++ b/docs/archive/current/packages/support/objects/hasAll.md @@ -0,0 +1,43 @@ +--- +title: Has All +description: Determine if all object paths are properties. +sidebarDepth: 0 +--- + +# `hasAll` + +Determine if all paths are properties of given object. + +_See also [`isset()`](./isset.md)._ + +```js +import { hasAll } from "@aedart/support/objects"; + +const mySymbol = Symbol('my-symbol'); +const target = { + a: 1234, + b: { + name: 'Sven', + c: { + age: 24, + [mySymbol]: true + } + }, + d: [ + { name: 'Jane'}, + { name: 'Ashley'}, + ], +}; + +const paths = [ + 'a', + 'b.name', + 'b.c.age', + ['b', 'c', mySymbol], + 'd[0]', + 'd[1].name', +]; + +let result = hasAll(target, paths); +console.log(result); // true +``` diff --git a/docs/archive/current/packages/support/objects/hasAny.md b/docs/archive/current/packages/support/objects/hasAny.md new file mode 100644 index 00000000..5af679db --- /dev/null +++ b/docs/archive/current/packages/support/objects/hasAny.md @@ -0,0 +1,32 @@ +--- +title: Has Any +description: Determine if any object paths are properties. +sidebarDepth: 0 +--- + +# `hasAny` + +Determine if any paths are properties of given object. + +```js +import { hasAny } from "@aedart/support/objects"; + +const target = { + a: 1234, + b: { + name: 'Sven', + c: { + age: 24 + } + } +}; + +const paths = [ + 'z', // does not exist + 'b.c.name', // does not exist + 'b.c.age', // exist +]; + +let result = hasAny(target, paths); +console.log(result); // true +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/objects/hasUniqueId.md b/docs/archive/current/packages/support/objects/hasUniqueId.md new file mode 100644 index 00000000..9544afe2 --- /dev/null +++ b/docs/archive/current/packages/support/objects/hasUniqueId.md @@ -0,0 +1,21 @@ +--- +title: Has Unique ID +description: Determine if object has a unique id. +sidebarDepth: 0 +--- + +# `hasUniqueId` + +Determine if an object has a unique id. + +_See [`uniqueId`](./uniqueId.md) for additional details._ + +```js +import { hasUniqueId } from "@aedart/support/objects"; + +const target = { + name: 'Ursula' +}; + +console.log(hasUniqueId(target)); // false +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/objects/isCloneable.md b/docs/archive/current/packages/support/objects/isCloneable.md new file mode 100644 index 00000000..77c4462c --- /dev/null +++ b/docs/archive/current/packages/support/objects/isCloneable.md @@ -0,0 +1,30 @@ +--- +title: Is Cloneable +description: Determine if object is cloneable. +sidebarDepth: 0 +--- + +# `isCloneable` + +Determines if given object is "cloneable". +In this context "cloneable" means that an object implements the `Cloneable` interface, and offers a `clone()` method. + +_See `@aedart/constracts/support/objects/Cloneable` for details._ + +```js +import { isCloneable } from "@aedart/support/objects"; + +class A {}; + +class B { + clone() { + return new this(); + } +} + +isCloneable(null); // false +isCloneable([]); // false +isCloneable({}); // false +isCloneable(new A()); // false +isCloneable(new B()); // true +``` diff --git a/docs/archive/current/packages/support/objects/isPopulatable.md b/docs/archive/current/packages/support/objects/isPopulatable.md new file mode 100644 index 00000000..fdf7ddde --- /dev/null +++ b/docs/archive/current/packages/support/objects/isPopulatable.md @@ -0,0 +1,32 @@ +--- +title: Is Populatable +description: Determine if object is populatable. +sidebarDepth: 0 +--- + +# `isPopulatable` + +Determines if given object is "populatable". +Here, "populatable" means that an object implements the `Populatable` interface, and offers a `populate()` method. + +_See `@aedart/constracts/support/objects/Populatable` for details._ + +```js +import { isPopulatable } from "@aedart/support/objects"; + +class A {}; + +class B { + populate(data) { + // ...not shown here... + + return this; + } +} + +isPopulatable(null); // false +isPopulatable([]); // false +isPopulatable({}); // false +isPopulatable(new A()); // false +isPopulatable(new B()); // true +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/objects/isset.md b/docs/archive/current/packages/support/objects/isset.md new file mode 100644 index 00000000..fff37807 --- /dev/null +++ b/docs/archive/current/packages/support/objects/isset.md @@ -0,0 +1,40 @@ +--- +title: Isset +description: Determine if object object paths are set and have values. +sidebarDepth: 0 +--- + +# `isset` + +Determine if paths are properties of given object and have values. +This method differs from [`has()`](./has.md), in that it only returns true if properties' values are not `undefined` and not `null`. + +_See also [misc. `isset()`](../misc/isset.md)._ + +```js +import { isset } from "@aedart/support/objects"; + +const target = { + a: 1234, + b: { + name: undefined, + c: { + age: null + } + }, +}; + +console.log(isset(target, 'a')); // true +console.log(isset(target, 'b')); // true +console.log(isset(target, 'b.name')); // false +console.log(isset(target, 'b.c')); // true +console.log(isset(target, 'b.c.age')); // false +``` + +You can also check if multiple paths are set. + +```js +console.log(isset(target, 'a', 'b')); // true +console.log(isset(target, 'b.c', 'b.name')); // false +console.log(isset(target, 'a', 'b.name', 'b.c.age')); // false +``` diff --git a/docs/archive/current/packages/support/objects/populate.md b/docs/archive/current/packages/support/objects/populate.md new file mode 100644 index 00000000..e5450d11 --- /dev/null +++ b/docs/archive/current/packages/support/objects/populate.md @@ -0,0 +1,126 @@ +--- +title: Populate +description: Populate target object. +sidebarDepth: 0 +--- + +# `populate` + +The `populate()` allows you to populate a target object's properties with those from a source object. +The values are [shallow copied](https://developer.mozilla.org/en-US/docs/Glossary/Shallow_copy). + +[[TOC]] + +## Arguments + +`populate()` accepts the following arguments: + +* `target: object` + +* `source: object` + +* `keys: PropertyKey | PropertyKey[] | SourceKeysCallback = '*'` - The keys to select and copy from `source` object. If wildcard (`*`) given, then all properties from the `source` are selected. + If a callback is given, then that callback must return key or keys to select from `source`. + +* `safe: boolean = true` - When `true`, properties must exist in target (_must be defined in target_), before they are shallow copied. + +::: warning Caution +The `target` object is mutated by this function. +::: + +::: tip Note +["Unsafe" properties](./reflections.md#iskeyunsafe) are disregarded, regardless of what `keys` are given. +::: + +```js +import { populate } from "@aedart/support/objects"; + +class Person { + name = null; + age = null; + + constructor(data) { + populate(this, data); + } +} + +const instance = new Person({ name: 'Janine', age: 36 }); +instance.name // Janine +instance.age // 36 +``` + +## Limit keys to populate + +By default, all keys (_`*`_) from the `source` object are attempted populated into the `target`. +You can limit what properties can be populated, by specifying what keys are allowed to be populated. + +```js +class Person { + name = null; + age = null; + phone = null; + + constructor(data) { + populate(this, data, [ 'name', 'age' ]); + } +} + +const instance = new Person({ name: 'Janine', age: 36, phone: '555 555 555' }); +instance.name // Janine +instance.age // 36 +instance.phone // null +``` + +## Source Keys Callback + +If you need a more advanced way to determine what keys to populate, then you can specify a callback as the `keys` argument. + +```js +populate(target, source, (source, target) => { + if (Reflect.has(source, 'phone') && Reflect.has(target, 'phone')) { + return [ 'name', 'age', 'phone' ]; + } + + return [ 'name', 'age' ]; +}); +``` + +## When keys do not exist + +When the `safe` argument is set to `true` (_default behavior_), and a property key does not exist in the `target` object, +then a `TypeError` is thrown. + +```js +class Person { + name = null; + age = null; + + constructor(data) { + populate(this, data, [ 'name', 'age', 'phone' ]); + } +} + +const instance = new Person({ + name: 'Janine', + age: 36, + phone: '555 555 555' +}); // TypeError - phone does not exist in target +``` + +However, if a requested key does not exist in the source object, then a `TypeError` is thrown regardless of the `safe` argument value. + +```js +class Person { + name = null; + age = null; + + constructor(data) { + populate(this, data, [ 'name', 'age', 'phone' ], false); + } +} + +const instance = new Person({ + name: 'Janine', + age: 36 +}); // TypeError - phone does not exist in source +``` diff --git a/docs/archive/current/packages/support/objects/set.md b/docs/archive/current/packages/support/objects/set.md new file mode 100644 index 00000000..7910d7a0 --- /dev/null +++ b/docs/archive/current/packages/support/objects/set.md @@ -0,0 +1,20 @@ +--- +title: Set +description: Set value in object path. +sidebarDepth: 0 +--- + +# `set` + +Set a value in object at given path. +_Method is an alias for [Lodash `set`](https://lodash.com/docs/4.17.15#set)._ + +```js +import { set } from "@aedart/support/objects"; + +const target = {}; + +set(target, 'a.foo', 'bar'); + +console.log(target); // { a: { foo: 'bar } } +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/objects/uniqueId.md b/docs/archive/current/packages/support/objects/uniqueId.md new file mode 100644 index 00000000..e6d8d09d --- /dev/null +++ b/docs/archive/current/packages/support/objects/uniqueId.md @@ -0,0 +1,27 @@ +--- +title: Unique ID +description: Set value in object path. +sidebarDepth: 0 +--- + +# `uniqueId` + +The `uniqueId()` is able to return a _"unique¹"_ reference identifier for any given object. + +```js +import { uniqueId, hasUniqueId } from "@aedart/support/objects"; + +const target = { + name: 'Ursula' +}; + +console.log(uniqueId(target)); // 27 + +// ...later in your application +console.log(hasUniqueId(target)); // true +console.log(uniqueId(target)); // 27 +``` + +The source code is heavily inspired by [Nicolas Gehlert's](https://github.com/ngehlert) blog post: ["_Get object reference IDs in JavaScript/TypeScript_" (September 28, 2022)](https://developapa.com/object-ids/) + +¹: _In this context, the returned number is unique in the current session. The number will NOT be unique across multiple sessions, nor guarantee that an object will receive the exact same identifier as in a previous session!_ \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections.md b/docs/archive/current/packages/support/reflections.md deleted file mode 100644 index 74b09453..00000000 --- a/docs/archive/current/packages/support/reflections.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Reflections -description: Reflection utilities -sidebarDepth: 0 ---- - -# Reflections - -The `@aedart/support/reflections` submodule offers a few reflection related utilities. - -[[TOC]] - -## `isConstructor` - -Based on the [TC39 `Function.isCallable() / Function.isConstructor()`](https://github.com/caitp/TC39-Proposals/blob/trunk/tc39-reflect-isconstructor-iscallable.md) proposal, the `isConstructor()` can determine if given argument is a constructor. - -```js{6,8-9} -import { isConstructor } from "@aedart/support/reflections"; - -isConstructor(null); // false -isConstructor({}); // false -isConstructor([]); // false -isConstructor(function() {}); // true -isConstructor(() => {}); // false -isConstructor(Array); // true -isConstructor(class {}); // true -``` - -**Acknowledgement** - -The source code of the above shown methods is heavily inspired by Denis Pushkarev's Core-js implementation of the [Function.isCallable / Function.isConstructor](https://github.com/zloirock/core-js#function-iscallable-isconstructor-) proposal (_License MIT_). - -## `isKeyUnsafe` - -Determines if a property key is considered "unsafe". - -```js -import { isKeyUnsafe } from '@aedart/support/reflections'; - -isKeyUnsafe('name'); // true -isKeyUnsafe('length'); // true -isKeyUnsafe('constructor'); // true -isKeyUnsafe('__proto__'); // false -``` - -::: tip Note -Behind the scene, the `isKeyUnsafe()` function matches the given key against values from the predefined `DANGEROUS_PROPERTIES` list, -which is defined in the `@aedart/contracts/support/objects` submodule; - -```js -import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; -``` -::: \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections/README.md b/docs/archive/current/packages/support/reflections/README.md new file mode 100644 index 00000000..0d1808f0 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/README.md @@ -0,0 +1,9 @@ +--- +title: About reflections +description: Reflection utilities. +sidebarDepth: 0 +--- + +# Reflections + +The `@aedart/support/reflections` submodule offers a few reflection related utilities. \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections/isConstructor.md b/docs/archive/current/packages/support/reflections/isConstructor.md new file mode 100644 index 00000000..ae7b7ff4 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/isConstructor.md @@ -0,0 +1,25 @@ +--- +title: Is Constructor +description: Determine if value is a constructor. +sidebarDepth: 0 +--- + +# `isConstructor` + +Based on the [TC39 `Function.isCallable() / Function.isConstructor()`](https://github.com/caitp/TC39-Proposals/blob/trunk/tc39-reflect-isconstructor-iscallable.md) proposal, the `isConstructor()` can determine if given argument is a constructor. + +```js{6,8-9} +import { isConstructor } from "@aedart/support/reflections"; + +isConstructor(null); // false +isConstructor({}); // false +isConstructor([]); // false +isConstructor(function() {}); // true +isConstructor(() => {}); // false +isConstructor(Array); // true +isConstructor(class {}); // true +``` + +**Acknowledgement** + +The source code of the above shown methods is heavily inspired by Denis Pushkarev's Core-js implementation of the [Function.isCallable / Function.isConstructor](https://github.com/zloirock/core-js#function-iscallable-isconstructor-) proposal (_License MIT_). diff --git a/docs/archive/current/packages/support/reflections/isKeyUnsafe.md b/docs/archive/current/packages/support/reflections/isKeyUnsafe.md new file mode 100644 index 00000000..2fbe1fb0 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/isKeyUnsafe.md @@ -0,0 +1,27 @@ +--- +title: Is Key Unsafe +description: Determine if a property key is unsafe. +sidebarDepth: 0 +--- + +# `isKeyUnsafe` + +Determines if a property key is considered "unsafe". + +```js +import { isKeyUnsafe } from '@aedart/support/reflections'; + +isKeyUnsafe('name'); // true +isKeyUnsafe('length'); // true +isKeyUnsafe('constructor'); // true +isKeyUnsafe('__proto__'); // false +``` + +::: tip Note +Behind the scene, the `isKeyUnsafe()` function matches the given key against values from the predefined `DANGEROUS_PROPERTIES` list, +which is defined in the `@aedart/contracts/support/objects` submodule; + +```js +import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; +``` +::: \ No newline at end of file From 9ec6e0b61faa72311efbc4df50c56a168027a1cd Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 1 Mar 2024 11:26:25 +0100 Subject: [PATCH 406/424] Change release notes --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6ce3782..d21f064a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `merge()`, `isTypedArray()`, `isArrayLike()`, `isSafeArrayLike()`, `isTypedArray()` and `isConcatSpreadable()` in `@aedart/support/arrays`. * `includesAny()` and `includesAll()` in `@aedart/support/arrays`. +### Changed + +* Split the "support" submodules docs into several smaller pages. + ### Fixed * Lodash JSDoc references in `get()`, `set()`, `unset()` and `forget()`, in `@aedart/support/objects`. From 88ee5a8792db93aa77d7f921fb9d4cb35ab09ce5 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 1 Mar 2024 11:30:44 +0100 Subject: [PATCH 407/424] Add doc for isKeySafe Also, fixed shown results for isKeyUnsafe() - ups! --- .../support/reflections/isKeyUnsafe.md | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/archive/current/packages/support/reflections/isKeyUnsafe.md b/docs/archive/current/packages/support/reflections/isKeyUnsafe.md index 2fbe1fb0..55fc6d3f 100644 --- a/docs/archive/current/packages/support/reflections/isKeyUnsafe.md +++ b/docs/archive/current/packages/support/reflections/isKeyUnsafe.md @@ -11,10 +11,10 @@ Determines if a property key is considered "unsafe". ```js import { isKeyUnsafe } from '@aedart/support/reflections'; -isKeyUnsafe('name'); // true -isKeyUnsafe('length'); // true -isKeyUnsafe('constructor'); // true -isKeyUnsafe('__proto__'); // false +isKeyUnsafe('name'); // false +isKeyUnsafe('length'); // false +isKeyUnsafe('constructor'); // false +isKeyUnsafe('__proto__'); // true ``` ::: tip Note @@ -24,4 +24,17 @@ which is defined in the `@aedart/contracts/support/objects` submodule; ```js import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; ``` -::: \ No newline at end of file +::: + +## `isKeySafe` + +Opposite of `isKeyUnsafe()`. + +```js +import { isKeySafe } from '@aedart/support/reflections'; + +isKeySafe('name'); // true +isKeySafe('length'); // true +isKeySafe('constructor'); // true +isKeySafe('__proto__'); // false +``` \ No newline at end of file From a926b3568b300d61e6bd435245e291584a971843 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 1 Mar 2024 12:11:27 +0100 Subject: [PATCH 408/424] Fix examples in JSDoc --- .../src/support/objects/merge/MergeOptions.ts | 29 ++++++++++--------- .../src/objects/merge/DefaultMergeOptions.ts | 25 ++++++++-------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/packages/contracts/src/support/objects/merge/MergeOptions.ts b/packages/contracts/src/support/objects/merge/MergeOptions.ts index e6175228..2d1dd6b4 100644 --- a/packages/contracts/src/support/objects/merge/MergeOptions.ts +++ b/packages/contracts/src/support/objects/merge/MergeOptions.ts @@ -31,14 +31,14 @@ export default interface MergeOptions * * **Example:** * ```js - * const a { 'foo': true }; - * const a { 'bar': true, 'zar': true }; - * - * merge([ a, b ], { skip: [ 'zar' ] }); // { 'foo': true, 'bar': true } + * const a = { 'foo': true }; + * const b = { 'bar': true, 'zar': true }; + * + * merge().using({ skip: [ 'zar' ] }).of(a, b); // { 'foo': true, 'bar': true } * - * merge([ a, b ], { skip: (key, source) => { + * merge().using({ skip: (key, source) => { * return key === 'bar' && Reflect.has(source, key); - * } }); // { 'foo': true, 'zar': true } + * } }).of(a, b); // { 'foo': true, 'zar': true } * ``` * * @type {PropertyKey[] | SkipKeyCallback} @@ -56,12 +56,12 @@ export default interface MergeOptions * * **Example:** * ```js - * const a { 'foo': true }; - * const a { 'foo': undefined }; + * const a = { 'foo': true }; + * const b = { 'foo': undefined }; * - * merge([ a, b ]); // { 'foo': undefined } + * merge(a, b); // { 'foo': undefined } * - * merge([ a, b ], { overwriteWithUndefined: false }); // { 'foo': true } + * merge().using({ overwriteWithUndefined: false }).of(a, b) // { 'foo': true } * ``` * * @type {boolean} @@ -90,8 +90,9 @@ export default interface MergeOptions * } * } }; * - * merge([ a, b ]); // { 'foo': { 'name': 'Rick Doe', 'age': 26 } } - * merge([ a, b ], { useCloneable: false }); // { 'foo': { 'name': 'Jane Doe', clone() {...} } } + * merge(a, b); // { 'foo': { 'name': 'Rick Doe', 'age': 26 } } + * + * merge().using({ useCloneable: false }).of(a, b); // { 'foo': { 'name': 'Jane Doe', clone() {...} } } * ``` * * @see [`Cloneable`]{@link import('@aedart/contracts/support/objects').Cloneable} @@ -113,8 +114,8 @@ export default interface MergeOptions * const a = { 'foo': [ 1, 2, 3 ] }; * const b = { 'foo': [ 4, 5, 6 ] }; * - * merge([ a, b ]); // { 'foo': [ 4, 5, 6 ] } - * merge([ a, b ], { mergeArrays: true }); // { 'foo': [ 1, 2, 3, 4, 5, 6 ] } + * merge(a, b); // { 'foo': [ 4, 5, 6 ] } + * merge().using({ mergeArrays: true }).of(a, b); // { 'foo': [ 1, 2, 3, 4, 5, 6 ] } * ``` * * **Note**: _`String()` (object) and [Typed Arrays]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray} diff --git a/packages/support/src/objects/merge/DefaultMergeOptions.ts b/packages/support/src/objects/merge/DefaultMergeOptions.ts index f0cc7f06..a96776a3 100644 --- a/packages/support/src/objects/merge/DefaultMergeOptions.ts +++ b/packages/support/src/objects/merge/DefaultMergeOptions.ts @@ -39,14 +39,14 @@ export default class DefaultMergeOptions implements MergeOptions * * **Example:** * ```js - * const a { 'foo': true }; - * const a { 'bar': true, 'zar': true }; + * const a = { 'foo': true }; + * const b = { 'bar': true, 'zar': true }; * - * merge([ a, b ], { skip: [ 'zar' ] }); // { 'foo': true, 'bar': true } + * merge().using({ skip: [ 'zar' ] }).of(a, b); // { 'foo': true, 'bar': true } * - * merge([ a, b ], { skip: (key, source) => { + * merge().using({ skip: (key, source) => { * return key === 'bar' && Reflect.has(source, key); - * } }); // { 'foo': true, 'zar': true } + * } }).of(a, b); // { 'foo': true, 'zar': true } * ``` * * @type {PropertyKey[] | SkipKeyCallback} @@ -64,12 +64,12 @@ export default class DefaultMergeOptions implements MergeOptions * * **Example:** * ```js - * const a { 'foo': true }; - * const a { 'foo': undefined }; + * const a = { 'foo': true }; + * const b = { 'foo': undefined }; * - * merge([ a, b ]); // { 'foo': undefined } + * merge(a, b); // { 'foo': undefined } * - * merge([ a, b ], { overwriteWithUndefined: false }); // { 'foo': true } + * merge().using({ overwriteWithUndefined: false }).of(a, b) // { 'foo': true } * ``` * * @type {boolean} @@ -83,7 +83,7 @@ export default class DefaultMergeOptions implements MergeOptions * **When `true` (_default behaviour_)**: _If source object is cloneable then the resulting object from `clone()` * method is used. Its properties are then iterated by the merge function._ * - * **When `false`**: _Cloneable objects are treated like any other objects (`clone()` method ignored)._ + * **When `false`**: _Cloneable objects are treated like any other objects, the `clone()` method is ignored._ * * **Example:** * ```js @@ -98,8 +98,9 @@ export default class DefaultMergeOptions implements MergeOptions * } * } }; * - * merge([ a, b ]); // { 'foo': { 'name': 'Rick Doe', 'age': 26 } } - * merge([ a, b ], { useCloneable: false }); // { 'foo': { 'name': 'Jane Doe', clone() {...} } } + * merge(a, b); // { 'foo': { 'name': 'Rick Doe', 'age': 26 } } + * + * merge().using({ useCloneable: false }).of(a, b); // { 'foo': { 'name': 'Jane Doe', clone() {...} } } * ``` * * @see [`Cloneable`]{@link import('@aedart/contracts/support/objects').Cloneable} From 8e7aca8d3e74165d41717bfa556f7a29697637f9 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 1 Mar 2024 13:01:34 +0100 Subject: [PATCH 409/424] Add docs for objects merge() util --- docs/.vuepress/archive/Version0x.ts | 1 + .../current/packages/support/objects/merge.md | 357 ++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 docs/archive/current/packages/support/objects/merge.md diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index d34a11ab..34d12678 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -102,6 +102,7 @@ export default PagesCollection.make('v0.x', '/v0x', [ 'packages/support/objects/isCloneable', 'packages/support/objects/isPopulatable', 'packages/support/objects/isset', + 'packages/support/objects/merge', 'packages/support/objects/populate', 'packages/support/objects/set', 'packages/support/objects/uniqueId', diff --git a/docs/archive/current/packages/support/objects/merge.md b/docs/archive/current/packages/support/objects/merge.md new file mode 100644 index 00000000..19804992 --- /dev/null +++ b/docs/archive/current/packages/support/objects/merge.md @@ -0,0 +1,357 @@ +--- +title: Merge +description: Merge multiple objects into a new object. +sidebarDepth: 0 +--- + +# `merge` + +Merges objects recursively into a new object. The properties and values of the source objects are copied, using [deep copy techniques](https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy), when possible. +Behind the scene, most value types are deep copied via [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone). + +[[TOC]] + +**Example** + +```js +import { merge } from "@aedart/support/objects"; + +const person = { + 'name': 'Alice', +}; + +const address = { + 'address': { + 'street': 'Somewhere Street 43' + }, +}; + +const result = merge(a, b); + +console.log(result); +``` + +The above shown example results in a new object that looks like this: + +```json +{ + "name": "Alice", + "address": { + "street": "Somewhere Street 43" + } +} +``` + +## Shallow Copied Types + +Be default, the following value types are only [shallow copied](https://developer.mozilla.org/en-US/docs/Glossary/Shallow_copy): + +- [function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function) +- [Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) + +```js +const a = { + 'foo': null, + 'bar': Symbol('my_symbol') +}; + +const b = { + 'foo': function() {}, +}; + +const result = merge(a, b); + +console.log(result.foo === b.foo); // true +console.log(result.bar === a.bar); // true +``` + +## Unsafe Keys + +Property keys that are considered "unsafe", are never copied. + +```js +const a = { + 'foo': 'bar' +}; +const b = { + __proto__: { 'is_admin': true } +} + +const result = merge(a, b); + +console.log(result); // { 'foo': 'bar' } +console.log(Reflect.has(result, '__proto__')); // false +``` + +_See [`isUnsafeKey()`](../reflections/isKeyUnsafe.md) for additional details._ + +## Merge Options + +`merge()` supports a number of options, which can be customised. You do so via the `using()` method. + +```js +merge() + .using({ /** option: value */ }) + .of(objA, objB, objC); +``` + +::: tip Note +When invoking `merge()` without any arguments, then the underlying objects `Merger` instance is returned. +::: + +### `depth` + +The `depth` option specifies the maximum merge depth. + +* Default maximum depth: `512` + +A `MergeError` is thrown, if the maximum depth is exceeded. + +```js +const a = { + 'person': { + 'name': 'Una' + } +}; + +const b = { + 'person': { // Level 0 + 'age': 24, // Level 1 + 'address': { + 'street': 'Somewhere Str. 654' // Level 2 + } + } +}; + +const result = merge() + .using({ + depth: 1 + }) + .of(a, b); // MergeError - Maximum merge depth (1) has been exceeded +``` + +### `skip` + +`skip` defines property keys that must not be merged. + +It accepts an array of property keys or a callback. + +#### List of property keys + +```js +const a = { + 'person': { + 'name': 'Ulrik' + } +}; + +const b = { + 'person': { + 'age': 36, + 'address': { + 'street': 'Nowhere Str. 12' + } + } +}; + +const result = merge() + .using({ + skip: [ 'age' ] + }) + .of(a, b); +``` + +The above example results in the following new object: + +```json +{ + "person": { + "name": "Ulrik", + "address": { + "street": "Nowhere Str. 12" + } + } +} +``` + +::: tip Note +When specifying a list of property keys, then the depth level in which the property key is found does not matter. +::: + +#### Skip Callback + +In situations when you need to perform more advanced skip logic, then you can use a callback. +It is given the following arguments: + +- `key: PropertyKey` - The current property that is being processed. +- `source: object` - The source object that contains the key. +- `result: object` - The resulting object (_relative to the current depth that is being processed_). + +The callback MUST return a boolean value; `true` if given key must be skipped, `false` otherwise. + +```js +const a = { + 'person': { + 'name': 'Jane' + } +}; + +const b = { + 'person': { + 'name': 'James', + 'address': { + 'street': 'Sunview Palace 88' + } + } +}; + +const b = { + 'person': { + 'name': 'White', + } +}; + +const result = merge() + .using({ + skip: (key, source, result) => { + return key === 'name' + && source[key] !== null + && !Reflect.has(result, key); + } + }) + .of(a, b); +``` + +The above example results in the following new object: + +```json +{ + "person": { + "name": "Jane", + "address": { + "street": "Sunview Palace 88" + } + } +} +``` + +### `overwriteWithUndefined` + +Determines if a property value should be overwritten with `undefined`. + +**Note**: _By default, all values are overwritten, even when they are `undefined`!_ + +```js +const a = { 'foo': true }; +const b = { 'foo': undefined }; + +merge(a, b); // { 'foo': undefined } + +merge() + .using({ overwriteWithUndefined: false }) + .of(a, b) // { 'foo': true } +``` + +### `useCloneable` + +Determines if an object's return value from a `clone()` method (_see [`Cloneable`](../objects/isCloneable.md)_) should be used for merging, +rather than the source object itself. + +**Note**: _By default, if an object is cloneable, then its return value from `clone()` is used._ + +```js +const a = { 'foo': { 'name': 'John Doe' } }; +const b = { 'foo': { + 'name': 'Jane Doe', + clone() { + return { + 'name': 'Rick Doe', + 'age': 26 + } + } +} }; + +merge(a, b); // { 'foo': { 'name': 'Rick Doe', 'age': 26 } } + +merge() + .using({ useCloneable: false }) + .of(a, b); // { 'foo': { 'name': 'Jane Doe', clone() {...} } } +``` + +### `mergeArrays` + +Determines whether to merge array, [array-like](../arrays/isArrayLike.md), and [concat spreadable](../arrays/isConcatSpreadable.md) properties or not. + +**Note**: _By default, existing property is overwritten with new property value._ + +```js +const a = { 'foo': [ 1, 2, 3 ] }; +const b = { 'foo': [ 4, 5, 6 ] }; + +merge(a, b); // { 'foo': [ 4, 5, 6 ] } + +merge() + .using({ mergeArrays: true }) + .of(a, b); // { 'foo': [ 1, 2, 3, 4, 5, 6 ] } +``` + +Behind the scene, the [array merge](../arrays/merge.md) utility is used for merging array properties. + +### `callback` + +In situations when you need more advanced merging of objects, you may specify a custom callback. + +The callback is _**responsible**_ for returning the value to be merged, from a given source object. + +```js +const a = { + 'a': 1 +}; + +const b = { + 'b': 2 +}; + +const result = merge() + .using((target, next, options) => { + const { key, value } = target; + if (key === 'b') { + return value + 1; + } + + return value; + }) + .of(a, b); // { 'a': 1, 'b': 3 } +``` + +#### Arguments + +The merge callback is given the following arguments: + +- `target: MergeSourceInfo` - The source target information (_see below_). +- `next: NextCallback` - Callback to invoke for merging nested objects (_next depth level_). +- `options: Readonly` - The merge options to be applied. + +**`target: MergeSourceInfo`** + +The source target information object contains the following properties: + +- `result: object` - The resulting object (_relative to object depth_) +- `key: PropertyKey` - The target property key in source object to. +- `value: any` - Value of the property in source object. +- `source: object` - The source object that holds the property key and value. +- `sourceIndex: number` - Source object's index (_relative to object depth_). +- `depth: number` - The current recursion depth. + +**`next: NextCallback`** + +The callback to perform the merging of nested objects. +It accepts the following arguments: + +- `sources: object[]` - The nested objects to be merged. +- `options: Readonly` - The merge options to be applied. +- `nextDepth: number` - The next recursion depth number. + +#### Onward + +For additional information about the merge callback, please review the source code of the `defaultMergeCallback()`, inside `@aedart/support/objects`. \ No newline at end of file From 393ea64f6f54b835d3725e6c8217fc264b3046d0 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 1 Mar 2024 13:08:02 +0100 Subject: [PATCH 410/424] Fix example --- docs/archive/current/packages/support/objects/merge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/archive/current/packages/support/objects/merge.md b/docs/archive/current/packages/support/objects/merge.md index 19804992..9388ecad 100644 --- a/docs/archive/current/packages/support/objects/merge.md +++ b/docs/archive/current/packages/support/objects/merge.md @@ -26,7 +26,7 @@ const address = { }, }; -const result = merge(a, b); +const result = merge(person, address); console.log(result); ``` From cf41cefe5f2594a0d24cd2254f8219e71f9a0a9d Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 1 Mar 2024 13:08:13 +0100 Subject: [PATCH 411/424] Highlight objects merge util --- docs/archive/current/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/archive/current/README.md b/docs/archive/current/README.md index c7e22267..4d487cec 100644 --- a/docs/archive/current/README.md +++ b/docs/archive/current/README.md @@ -29,6 +29,26 @@ _TBD: "To be decided"._ ## `v0.x` Highlights +### Merge + +Objects [merge](./packages/support/objects/merge.md) utility, using [deep copy](https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy). + +```js +import { merge } from "@aedart/support/objects"; + +const a = { + 'name': 'Alin', +}; + +const b = { + 'address': { + 'street': 'Northern Street 1' + }, +}; + +const result = merge(a, b); // { 'name': 'Alin', 'address': { 'street': '...' } } +``` + ### Mixins Adaptation of Justin Fagnani's [`mixwith.js`](https://github.com/justinfagnani/mixwith.js). From 39cbc886953afb81fbffe0352783e3e4fcaca70d Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 1 Mar 2024 13:10:08 +0100 Subject: [PATCH 412/424] Improve warning --- docs/archive/current/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/archive/current/README.md b/docs/archive/current/README.md index 4d487cec..bcdcca3a 100644 --- a/docs/archive/current/README.md +++ b/docs/archive/current/README.md @@ -8,7 +8,7 @@ sidebarDepth: 0 ::: danger Ion is still in development. -You _SHOULD NOT_ use any of the packages in production. +You _SHOULD NOT_ use any of the packages in a production environment. Breaking changes _**MUST**_ be expected for all `v0.x` releases! _Please review the [`CHANGELOG.md`](https://github.com/aedart/ion/blob/main/CHANGELOG.md) for additional details._ From bda0ecb3e57451d0c75aaebae33c6d00a5f17264 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 1 Mar 2024 13:11:16 +0100 Subject: [PATCH 413/424] Fix broken links --- docs/archive/current/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/archive/current/README.md b/docs/archive/current/README.md index bcdcca3a..e86f63f4 100644 --- a/docs/archive/current/README.md +++ b/docs/archive/current/README.md @@ -79,12 +79,12 @@ item.name = 'My Item'; console.log(item.name); // My Item ``` -See details and more examples in the [`@aedart/support/mixins` documentation](./packages/support/mixins.md). +See details and more examples in the [`@aedart/support/mixins` documentation](./packages/support/mixins/README.md). ### "Target" Meta Decorator Associate arbitrary metadata directly with the target element that is being decorated. -_See [target meta decorator](./packages/support/meta.md) fro additional details._ +_See [target meta decorator](./packages/support/meta/targetMeta.md) fro additional details._ ```js import {targetMeta, getTargetMeta} from '@aedart/support/meta'; @@ -105,7 +105,7 @@ getTargetMeta(instance.search, 'desc'); // Seaches for cities ### Meta Decorator -The [meta decorator](./packages/support/meta.md) is able to associate arbitrary metadata with a class and its elements. +The [meta decorator](./packages/support/meta/README.md) is able to associate arbitrary metadata with a class and its elements. ```js import {meta, getMeta} from '@aedart/support/meta'; From e60b686bc2f6a563a51b3793c241a7ff03a9d52b Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 4 Mar 2024 08:42:58 +0100 Subject: [PATCH 414/424] Improve sentences --- docs/archive/current/packages/support/objects/merge.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/archive/current/packages/support/objects/merge.md b/docs/archive/current/packages/support/objects/merge.md index 9388ecad..eb550c9f 100644 --- a/docs/archive/current/packages/support/objects/merge.md +++ b/docs/archive/current/packages/support/objects/merge.md @@ -87,7 +87,7 @@ _See [`isUnsafeKey()`](../reflections/isKeyUnsafe.md) for additional details._ ## Merge Options -`merge()` supports a number of options, which can be customised. You do so via the `using()` method. +`merge()` supports a number of options. To specify thom, use the `using()` method. ```js merge() @@ -96,7 +96,7 @@ merge() ``` ::: tip Note -When invoking `merge()` without any arguments, then the underlying objects `Merger` instance is returned. +When invoking `merge()` without any arguments, an underlying objects `Merger` instance is returned. ::: ### `depth` From edeb3904610a1b35684b0f7c731074a3865a91dc Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 4 Mar 2024 08:54:37 +0100 Subject: [PATCH 415/424] Improve sentences Also, corrected the custom merge callback example. Now showing how to provide as "callback: ..." option, as well as the shorthand version. --- .../current/packages/support/objects/merge.md | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/archive/current/packages/support/objects/merge.md b/docs/archive/current/packages/support/objects/merge.md index eb550c9f..bc3e00b4 100644 --- a/docs/archive/current/packages/support/objects/merge.md +++ b/docs/archive/current/packages/support/objects/merge.md @@ -180,8 +180,8 @@ When specifying a list of property keys, then the depth level in which the prope #### Skip Callback -In situations when you need to perform more advanced skip logic, then you can use a callback. -It is given the following arguments: +You can use a callback, if you need to handle more advanced skip logic. +The callback accepts the the following arguments: - `key: PropertyKey` - The current property that is being processed. - `source: object` - The source object that contains the key. @@ -280,9 +280,9 @@ merge() ### `mergeArrays` -Determines whether to merge array, [array-like](../arrays/isArrayLike.md), and [concat spreadable](../arrays/isConcatSpreadable.md) properties or not. +When enabled, arrays, [array-like](../arrays/isArrayLike.md), and [concat spreadable](../arrays/isConcatSpreadable.md) objects are merged. -**Note**: _By default, existing property is overwritten with new property value._ +**Note**: _By default, existing array values are NOT merged._ ```js const a = { 'foo': [ 1, 2, 3 ] }; @@ -295,11 +295,11 @@ merge() .of(a, b); // { 'foo': [ 1, 2, 3, 4, 5, 6 ] } ``` -Behind the scene, the [array merge](../arrays/merge.md) utility is used for merging array properties. +Behind the scene, the [array merge](../arrays/merge.md) utility is used for merging arrays. ### `callback` -In situations when you need more advanced merging of objects, you may specify a custom callback. +In situations when you need more advanced merge logic, you may specify a custom callback. The callback is _**responsible**_ for returning the value to be merged, from a given source object. @@ -312,6 +312,24 @@ const b = { 'b': 2 }; +const result = merge() + .using({ + callback: (target, next, options) => { + const { key, value } = target; + if (key === 'b') { + return value + 1; + } + + return value; + } + }) + .of(a, b); // { 'a': 1, 'b': 3 } +``` + +If you do not have other merge options to specify, then you can simply provide a merge callback directly as argument for +the `using()` method. + +```js const result = merge() .using((target, next, options) => { const { key, value } = target; @@ -321,7 +339,7 @@ const result = merge() return value; }) - .of(a, b); // { 'a': 1, 'b': 3 } + .of(a, b); ``` #### Arguments From 16aecf5cbbedd7ccc3a9ba4e92abaf34e933ca95 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 4 Mar 2024 09:49:17 +0100 Subject: [PATCH 416/424] Improve description of properties --- packages/contracts/src/support/reflections/ClassBlueprint.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/reflections/ClassBlueprint.ts b/packages/contracts/src/support/reflections/ClassBlueprint.ts index 18943de8..a63b565d 100644 --- a/packages/contracts/src/support/reflections/ClassBlueprint.ts +++ b/packages/contracts/src/support/reflections/ClassBlueprint.ts @@ -4,14 +4,14 @@ export default interface ClassBlueprint { /** - * Properties or methods that are statically defined in class + * Properties or methods expected to exist in class as static members. * * @type {PropertyKey[]} */ staticMembers?: PropertyKey[]; /** - * Properties or methods defined on class' prototype + * Properties or methods expected to exist in class' prototype * * @type {PropertyKey[]} */ From bd1cbe1232592fd05647602e35862627cb4dd2ee Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 4 Mar 2024 11:35:06 +0100 Subject: [PATCH 417/424] Add docs for reflection utils --- docs/.vuepress/archive/Version0x.ts | 16 ++++ .../reflections/assertHasPrototypeProperty.md | 18 ++++ .../support/reflections/classLooksLike.md | 84 +++++++++++++++++++ .../support/reflections/classOwnKeys.md | 36 ++++++++ .../reflections/getAllParentsOfClass.md | 28 +++++++ .../reflections/getClassPropertyDescriptor.md | 45 ++++++++++ .../getClassPropertyDescriptors.md | 52 ++++++++++++ .../support/reflections/getConstructorName.md | 24 ++++++ .../support/reflections/getNameOrDesc.md | 18 ++++ .../support/reflections/getParentOfClass.md | 25 ++++++ .../support/reflections/hasAllMethods.md | 26 ++++++ .../packages/support/reflections/hasMethod.md | 27 ++++++ .../reflections/hasPrototypeProperty.md | 24 ++++++ .../packages/support/reflections/isKeySafe.md | 18 ++++ .../support/reflections/isKeyUnsafe.md | 15 +--- .../support/reflections/isSubclass.md | 28 +++++++ .../reflections/isSubclassOrLooksLike.md | 34 ++++++++ .../support/reflections/isWeakKind.md | 26 ++++++ 18 files changed, 530 insertions(+), 14 deletions(-) create mode 100644 docs/archive/current/packages/support/reflections/assertHasPrototypeProperty.md create mode 100644 docs/archive/current/packages/support/reflections/classLooksLike.md create mode 100644 docs/archive/current/packages/support/reflections/classOwnKeys.md create mode 100644 docs/archive/current/packages/support/reflections/getAllParentsOfClass.md create mode 100644 docs/archive/current/packages/support/reflections/getClassPropertyDescriptor.md create mode 100644 docs/archive/current/packages/support/reflections/getClassPropertyDescriptors.md create mode 100644 docs/archive/current/packages/support/reflections/getConstructorName.md create mode 100644 docs/archive/current/packages/support/reflections/getNameOrDesc.md create mode 100644 docs/archive/current/packages/support/reflections/getParentOfClass.md create mode 100644 docs/archive/current/packages/support/reflections/hasAllMethods.md create mode 100644 docs/archive/current/packages/support/reflections/hasMethod.md create mode 100644 docs/archive/current/packages/support/reflections/hasPrototypeProperty.md create mode 100644 docs/archive/current/packages/support/reflections/isKeySafe.md create mode 100644 docs/archive/current/packages/support/reflections/isSubclass.md create mode 100644 docs/archive/current/packages/support/reflections/isSubclassOrLooksLike.md create mode 100644 docs/archive/current/packages/support/reflections/isWeakKind.md diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index 34d12678..be42a205 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -113,8 +113,24 @@ export default PagesCollection.make('v0.x', '/v0x', [ collapsible: true, children: [ 'packages/support/reflections/', + 'packages/support/reflections/assertHasPrototypeProperty', + 'packages/support/reflections/classLooksLike', + 'packages/support/reflections/classOwnKeys', + 'packages/support/reflections/getAllParentsOfClass', + 'packages/support/reflections/getClassPropertyDescriptor', + 'packages/support/reflections/getClassPropertyDescriptors', + 'packages/support/reflections/getConstructorName', + 'packages/support/reflections/getNameOrDesc', + 'packages/support/reflections/getParentOfClass', + 'packages/support/reflections/hasAllMethods', + 'packages/support/reflections/hasMethod', + 'packages/support/reflections/hasPrototypeProperty', 'packages/support/reflections/isConstructor', + 'packages/support/reflections/isKeySafe', 'packages/support/reflections/isKeyUnsafe', + 'packages/support/reflections/isSubclass', + 'packages/support/reflections/isSubclassOrLooksLike', + 'packages/support/reflections/isWeakKind', ] }, { diff --git a/docs/archive/current/packages/support/reflections/assertHasPrototypeProperty.md b/docs/archive/current/packages/support/reflections/assertHasPrototypeProperty.md new file mode 100644 index 00000000..778c8992 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/assertHasPrototypeProperty.md @@ -0,0 +1,18 @@ +--- +title: Assert Has Prototype Prop. +description: Assert that object has a "prototype" property. +sidebarDepth: 0 +--- + +# `assertHasPrototypeProperty` + +Assert that given target object has a `prototype` property defined. +Throws a `TypeError` if target object does not have a `prototype` property + +_See [`hasPrototypeProperty`](./hasPrototypeProperty.md) for details._ + +```js +import { assertHasPrototypeProperty } from '@aedart/support/reflections'; + +assertHasPrototypeProperty({ __proto__: null }); // TypeError +``` diff --git a/docs/archive/current/packages/support/reflections/classLooksLike.md b/docs/archive/current/packages/support/reflections/classLooksLike.md new file mode 100644 index 00000000..e06508ce --- /dev/null +++ b/docs/archive/current/packages/support/reflections/classLooksLike.md @@ -0,0 +1,84 @@ +--- +title: Class Looks Like +description: Determine if a class looks like blueprint. +sidebarDepth: 0 +--- + +# `classLooksLike` + +Determines if a target class _"looks like"_ the provided class "blueprint". + +[[TOC]] + +## Arguments + +`classLooksLike()` accepts the following arguments: + +- `target: object` - the target class object. +- `blueprint: ClassBlueprint` - a blueprint that defines the expected members of a class (_see [Class Blueprint](#class-blueprint) for details._). + +```js +import { classLooksLike } from '@aedart/support/reflections'; + +class A {} + +class B { + foo() {} +} + +const blueprint = { members: [ 'foo' ] }; + +classLooksLike(A, blueprint); // false +classLooksLike(B, blueprint); // true +``` + +## Class Blueprint + +The class "blueprint" is an object that defines the expected members (_property keys_) of a target class. +All defined members must exist in target class' prototype, before the `classLooksLike()` returns `true`. + +You can specify either or both of the following properties in a class blueprint object: + +- `members: PropertyKey[]` - (_optional_) Properties or methods expected to exist in class' prototype. +- `staticMembers: PropertyKey[]` - (_optional_) Properties or methods expected to exist in class as static members. + +**Note:** _If you do not specify either `members` or `staticMembers`, then a `TypeError` is thrown._ + +```js +class A { + foo() {} + + bar() {} +} + +class B { + foo() {} + + static bar() {} +} + +const blueprint = { members: [ 'foo' ], staticMembers: [ 'bar' ] }; + +classLooksLike(A, blueprint); // false +classLooksLike(B, blueprint); // true +``` + +## Recursive + +`classLooksLike()` traverses target class' prototype chain. This means that you can compare a subclass against a blueprint +and inherited members will automatically be included in the check. + +```js +class A { + foo() {} +} + +class B extends A { + bar() {} +} + +const blueprint = { members: [ 'foo', 'bar' ]}; + +classLooksLike(A, blueprint); // false +classLooksLike(B, blueprint); // true +``` diff --git a/docs/archive/current/packages/support/reflections/classOwnKeys.md b/docs/archive/current/packages/support/reflections/classOwnKeys.md new file mode 100644 index 00000000..1457cad6 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/classOwnKeys.md @@ -0,0 +1,36 @@ +--- +title: Class Own Keys +description: Get property keys of target class. +sidebarDepth: 0 +--- + +# `classOwnKeys` + +Returns property keys that are defined target class's prototype. +It accepts the following arguments: + +- `target: ConstructorOrAbstractConstructor` - The target class +- `recursive: boolean = false` - (_optional_) If `true`, then target's prototype chain is traversed and all property keys are returned. + +```js +import { classOwnKeys } from '@aedart/support/reflections'; + +class A { + foo() {} +} + +class B extends A { + get bar() {} +} + +classOwnKeys(B); // [ 'constructor', 'bar' ] +classOwnKeys(B, true); // [ 'constructor', 'foo', 'bar' ] +``` + +::: warning Caution +`classOwnKeys()` throws `TypeError` if target does not have a `prototype` property. +::: + +## Limitation + +The `classOwnKeys()` function does not return static members of a target class. diff --git a/docs/archive/current/packages/support/reflections/getAllParentsOfClass.md b/docs/archive/current/packages/support/reflections/getAllParentsOfClass.md new file mode 100644 index 00000000..a41450b5 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/getAllParentsOfClass.md @@ -0,0 +1,28 @@ +--- +title: Get All Parents Of Class +description: Get all parents of a class +sidebarDepth: 0 +--- + +# `getAllParentsOfClass` + +Returns all parents of target class. +It accepts the following arguments: + +- `target: ConstructorOrAbstractConstructor` - The target class. +- `includeTarget: boolean = false` - (_optional_) If `true`, then given target is included in the output as the first element. + +```js +import { getAllParentsOfClass } from '@aedart/support/reflections'; + +class A {} + +class B extends A {} + +class C extends B {} + +getAllParentsOfClass(C); // [ B, A ] +getAllParentsOfClass(C, true); // [ C, B, A ] +``` + +_See also [`getParentOfClass()`](./getParentOfClass.md)._ \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections/getClassPropertyDescriptor.md b/docs/archive/current/packages/support/reflections/getClassPropertyDescriptor.md new file mode 100644 index 00000000..14835736 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/getClassPropertyDescriptor.md @@ -0,0 +1,45 @@ +--- +title: Get Class Prop. Descriptor +description: Get property descriptor of target class property. +sidebarDepth: 0 +--- + +# `getClassPropertyDescriptor` + +Returns [`PropertyDescriptor`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getOwnPropertyDescriptor), +from target's prototype that matches given property key. + +It accepts the following arguments: + +- `target: ConstructorOrAbstractConstructor` - The target class. +- `key: PropertyKey` - Name of the property. + +```js +import { getClassPropertyDescriptor } from '@aedart/support/reflections'; + +class A { + set name(v) {} + get name() {} +} + +getClassPropertyDescriptor(A, 'name'); // see "output"... +``` + +The above show example results in the given output: + +```js +const output = { + get: function () { /* ...Not shown... */ }, + set: function (v) { /* ..Not shown... */ }, + enumerable: false, + configurable: true +}; +``` + +::: tip Note +`getClassPropertyDescriptor()` returns `undefined` if requested key does not exist in class' prototype. +::: + +::: warning Caution +`getClassPropertyDescriptor()` throws `TypeError` if target does not have a `prototype` property. +::: \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections/getClassPropertyDescriptors.md b/docs/archive/current/packages/support/reflections/getClassPropertyDescriptors.md new file mode 100644 index 00000000..2abdbbec --- /dev/null +++ b/docs/archive/current/packages/support/reflections/getClassPropertyDescriptors.md @@ -0,0 +1,52 @@ +--- +title: Get Class Prop. Descriptors +description: Get all property descriptors of target class. +sidebarDepth: 0 +--- + +# `getClassPropertyDescriptors` + +Returns all property [`descriptors`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getOwnPropertyDescriptor) that are defined target's prototype. + +It accepts the following arguments: + +- `target: ConstructorOrAbstractConstructor` - The target class. +- `recursive: boolean = false` - (_optional_) If `true`, then target's parent prototypes are traversed. Descriptors are merged, such that the top-most class' descriptors are returned. + +```js +import { getClassPropertyDescriptors } from '@aedart/support/reflections'; + +class A { + set name(v) {} + get name() {} + bar() {} + [MY_SYMBOL]() {} +} + +getClassPropertyDescriptors(A); // { bar: {...}, name: {...}, [MY_SYMBOL]: {...} } +``` + +When `recursive` is set to `true`, then all property descriptors are returned from the target class' prototype chain. + +```js +import { getClassPropertyDescriptors } from '@aedart/support/reflections'; + +class A { + set name(v) {} + get name() {} + foo() {} + [MY_SYMBOL]() {} +} + +class B extends A { + set bar(v) {} + get bar() {} +} + +getClassPropertyDescriptors(B, true); +// { bar: {...}, foo: {...}, name: {...}, [MY_SYMBOL]: {...} } +``` + +::: warning Caution +`getClassPropertyDescriptors()` throws `TypeError` if target does not have a `prototype` property. +::: \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections/getConstructorName.md b/docs/archive/current/packages/support/reflections/getConstructorName.md new file mode 100644 index 00000000..2fb8adcd --- /dev/null +++ b/docs/archive/current/packages/support/reflections/getConstructorName.md @@ -0,0 +1,24 @@ +--- +title: Get Constructor Name +description: Get name of target class' constructor +sidebarDepth: 0 +--- + +# `getConstructorName` + +Returns target class' constructor name, if available. + +It accepts the following arguments: + +- `target: ConstructorOrAbstractConstructor` - The target class +- `defaultValue: string|null = null` - (_optional_) A default string value to return if target has no constructor name. + +```js +import { getConstructorName } from '@aedart/support/reflections'; + +class Box {} + +getConstructorName(Box); // Box +getConstructorName(class {}); // null +getConstructorName(class {}, 'MyBox'); // MyBox +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections/getNameOrDesc.md b/docs/archive/current/packages/support/reflections/getNameOrDesc.md new file mode 100644 index 00000000..6f8cd734 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/getNameOrDesc.md @@ -0,0 +1,18 @@ +--- +title: Get Name Or Desc. Tag +description: Get name of target class' constructor, or description tag +sidebarDepth: 0 +--- + +# `getNameOrDesc` + +Returns target class' [constructor name](./getConstructorName.md), or [description tag](../misc/descTag.md) if name is not available. + +```js +import { getNameOrDesc } from '@aedart/support/reflections'; + +class ApiService {} + +getNameOrDesc(ApiService); // ApiService +getNameOrDesc(class {}); // [object Function] +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections/getParentOfClass.md b/docs/archive/current/packages/support/reflections/getParentOfClass.md new file mode 100644 index 00000000..5248e5db --- /dev/null +++ b/docs/archive/current/packages/support/reflections/getParentOfClass.md @@ -0,0 +1,25 @@ +--- +title: Get Parent Of Class +description: Get parent class. +sidebarDepth: 0 +--- + +# `getParentOfClass` + +Returns the parent class of given target class, or `null` if class does not have a parent. + +```js +import { getParentOfClass } from '@aedart/support/reflections'; + +class A {} + +class B extends A {} + +class C extends B {} + +getParentOfClass(A); // null +getParentOfClass(B); // A +getParentOfClass(C); // B +``` + +_See also [`getAllParentsOfClass()`](./getAllParentsOfClass.md)._ \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections/hasAllMethods.md b/docs/archive/current/packages/support/reflections/hasAllMethods.md new file mode 100644 index 00000000..c8cc4d71 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/hasAllMethods.md @@ -0,0 +1,26 @@ +--- +title: Has All Methods +description: Determine if target has all methods +sidebarDepth: 0 +--- + +# `hasAllMethods` + +Determine if given target object contains all given methods. + +It accepts the following arguments: + +- `target: object` - The target. +- `...methods: PropertyKey[]` - Names of the methods to check for. + +```js +import { hasAllMethods } from '@aedart/support/reflections'; + +const a = { + foo: () => { /* ...not shown... */ }, + bar: () => { /* ...not shown... */ }, +} + +hasAllMethods(a, 'foo', 'bar'); // true +hasAllMethods(a, 'foo', 'bar', 'zar'); // false +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections/hasMethod.md b/docs/archive/current/packages/support/reflections/hasMethod.md new file mode 100644 index 00000000..5aec48d3 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/hasMethod.md @@ -0,0 +1,27 @@ +--- +title: Has Method +description: Determine if target has method +sidebarDepth: 0 +--- + +# `hasMethod` + +Determine if given target object contains method. + +It accepts the following arguments: + +- `target: object` - The target. +- `method: PropertyKey` - Name of the method to check for. + +```js +import { hasMethod } from '@aedart/support/reflections'; + +const a = { + foo: () => { /* ...not shown... */ }, + bar: () => { /* ...not shown... */ }, +} + +hasMethod(a, 'foo'); // true +hasMethod(a, 'bar'); // true +hasMethod(a, 'zar'); // false +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections/hasPrototypeProperty.md b/docs/archive/current/packages/support/reflections/hasPrototypeProperty.md new file mode 100644 index 00000000..2ba3d5c9 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/hasPrototypeProperty.md @@ -0,0 +1,24 @@ +--- +title: Has Prototype Property +description: Determine if "prototype" property exists. +sidebarDepth: 0 +--- + +# `hasPrototypeProperty` + +Determines if object has a `prototype` property defined and that it is not `null` or `undefined`. + +```js +import { hasPrototypeProperty } from '@aedart/support/reflections'; + +hasPrototypeProperty(null); // false +hasPrototypeProperty(Object.create(null)); // false +hasPrototypeProperty({ __proto__: undefined }); // false +hasPrototypeProperty({ prototype: null }); // false +hasPrototypeProperty(() => true); // false + +hasPrototypeProperty(Object.create({ prototype: {} })); // true +hasPrototypeProperty({ __proto__: function() {} }); // true +hasPrototypeProperty(function() {}); // true +hasPrototypeProperty(class {}); // true +``` diff --git a/docs/archive/current/packages/support/reflections/isKeySafe.md b/docs/archive/current/packages/support/reflections/isKeySafe.md new file mode 100644 index 00000000..bfc297a8 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/isKeySafe.md @@ -0,0 +1,18 @@ +--- +title: Is Key Safe +description: Determine if a property key is safe. +sidebarDepth: 0 +--- + +## `isKeySafe` + +Opposite of [`isKeyUnsafe()`](./isKeyUnsafe.md). + +```js +import { isKeySafe } from '@aedart/support/reflections'; + +isKeySafe('name'); // true +isKeySafe('length'); // true +isKeySafe('constructor'); // true +isKeySafe('__proto__'); // false +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections/isKeyUnsafe.md b/docs/archive/current/packages/support/reflections/isKeyUnsafe.md index 55fc6d3f..bbbd1abb 100644 --- a/docs/archive/current/packages/support/reflections/isKeyUnsafe.md +++ b/docs/archive/current/packages/support/reflections/isKeyUnsafe.md @@ -24,17 +24,4 @@ which is defined in the `@aedart/contracts/support/objects` submodule; ```js import { DANGEROUS_PROPERTIES } from "@aedart/contracts/support/objects"; ``` -::: - -## `isKeySafe` - -Opposite of `isKeyUnsafe()`. - -```js -import { isKeySafe } from '@aedart/support/reflections'; - -isKeySafe('name'); // true -isKeySafe('length'); // true -isKeySafe('constructor'); // true -isKeySafe('__proto__'); // false -``` \ No newline at end of file +::: \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections/isSubclass.md b/docs/archive/current/packages/support/reflections/isSubclass.md new file mode 100644 index 00000000..cf8dbac5 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/isSubclass.md @@ -0,0 +1,28 @@ +--- +title: Is Subclass +description: Determine if target is a subclass of another class. +sidebarDepth: 0 +--- + +# `isSubclass` + +Determine if target class is a subclass (_child class_) of given superclass (_parent class_). + +It accepts the following arguments: + +- `target: object` - The target. +- `superclass: ConstructorOrAbstractConstructor` - The superclass. + +```js +import { isSubclass } from '@aedart/support/reflections'; + +class A {} + +class B extends A {} + +isSubclass({}, A); // false +isSubclass(A, A); // false +isSubclass(A, B); // false + +isSubclass(B, A); // true +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections/isSubclassOrLooksLike.md b/docs/archive/current/packages/support/reflections/isSubclassOrLooksLike.md new file mode 100644 index 00000000..57376450 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/isSubclassOrLooksLike.md @@ -0,0 +1,34 @@ +--- +title: Is Subclass Or Looks Like +description: Determine if target is a subclass of another class, or looks like blueprint +sidebarDepth: 0 +--- + +# `isSubclassOrLooksLike` + +Determine if target class is a subclass of given superclass, or if it looks like given blueprint. + +It accepts the following arguments: + +- `target: object` - The target. +- `superclass: ConstructorOrAbstractConstructor` - The superclass. +- `blueprint: ClassBlueprint` - Class Blueprint (_See [`classLooksLike`](./classLooksLike.md#class-blueprint)_). + +```js +import { isSubclassOrLooksLike } from '@aedart/support/reflections'; + +class A { + foo() {} +} +class B extends A {} + +class C { + foo() {} +} + +isSubclassOrLooksLike(B, A, { members: [] }); // true +isSubclassOrLooksLike(C, A, { members: [] }); // false +isSubclassOrLooksLike(C, A, { members: [ 'foo' ] }); // true +``` + +_See [`isSubclass()`](./isSubclass.md) and [`classLooksLike()`](./classLooksLike.md) for additional details._ \ No newline at end of file diff --git a/docs/archive/current/packages/support/reflections/isWeakKind.md b/docs/archive/current/packages/support/reflections/isWeakKind.md new file mode 100644 index 00000000..3abab271 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/isWeakKind.md @@ -0,0 +1,26 @@ +--- +title: Is WeakKind +description: Determine if object is of a "weak" kind. +sidebarDepth: 0 +--- + +# `isWeakKind` + +Determine if object of a "weak" kind, e.g. [`WeakRef`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef), +[`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap), +or [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet). + +```js +import { isWeakKind } from '@aedart/support/reflections'; + +const a = {}; + +isWeakKind(null); // false +isWeakKind(a); // false +isWeakKind(new Map()); // false +isWeakKind(new Set()); // false + +isWeakKind(new WeakRef(a)); // true +isWeakKind(new WeakMap()); // true +isWeakKind(new WeakSet()); // true +``` \ No newline at end of file From ce2a06dadbab3627f76a3215ae8cd76a13acb6d5 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 5 Mar 2024 10:17:16 +0100 Subject: [PATCH 418/424] Fix BEFORE description --- packages/support/src/concerns/AbstractConcern.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index 70f2c231..933cf2fe 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -81,7 +81,7 @@ export default abstract class AbstractConcern implements Concern * * **Note**: _This hook method is intended to be invoked by an * [Injector]{@link import('@aedart/contracts/support/concerns').Injector}, before - * any concern classes are registered in the target class._ + * a concern container is and aliases are defined in the target class._ * * @static * From acd727a89733756e25a23b0befde422ffb342e0f Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 5 Mar 2024 10:18:22 +0100 Subject: [PATCH 419/424] Fix typo --- packages/contracts/src/support/concerns/RegistrationAware.ts | 2 +- packages/support/src/concerns/AbstractConcern.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/concerns/RegistrationAware.ts b/packages/contracts/src/support/concerns/RegistrationAware.ts index 44831d0e..7ba8c1c6 100644 --- a/packages/contracts/src/support/concerns/RegistrationAware.ts +++ b/packages/contracts/src/support/concerns/RegistrationAware.ts @@ -14,7 +14,7 @@ export default interface RegistrationAware * * **Note**: _This hook method is intended to be invoked by an * [Injector]{@link import('@aedart/contracts/support/concerns').Injector}, before - * a concern container is and aliases are defined in the target class._ + * the concern container and aliases are defined in the target class._ * * @static * diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index 933cf2fe..aa055489 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -81,7 +81,7 @@ export default abstract class AbstractConcern implements Concern * * **Note**: _This hook method is intended to be invoked by an * [Injector]{@link import('@aedart/contracts/support/concerns').Injector}, before - * a concern container is and aliases are defined in the target class._ + * the concern container and aliases are defined in the target class._ * * @static * From 57b2b92a62d6bedbbb0d8acb9d3bb5580c72e45d Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 5 Mar 2024 13:22:56 +0100 Subject: [PATCH 420/424] Add docs. for concerns submodule --- docs/.vuepress/archive/Version0x.ts | 16 ++ .../packages/support/concerns/README.md | 46 ++++ .../packages/support/concerns/aliases.md | 94 ++++++++ .../packages/support/concerns/booting.md | 93 ++++++++ .../packages/support/concerns/concernClass.md | 168 ++++++++++++++ .../support/concerns/conflictResolution.md | 111 +++++++++ .../packages/support/concerns/edgeCases.md | 177 +++++++++++++++ .../packages/support/concerns/hooks.md | 62 +++++ .../packages/support/concerns/jsdoc.md | 211 ++++++++++++++++++ .../support/concerns/prerequisites.md | 10 + .../packages/support/concerns/usage.md | 189 ++++++++++++++++ 11 files changed, 1177 insertions(+) create mode 100644 docs/archive/current/packages/support/concerns/README.md create mode 100644 docs/archive/current/packages/support/concerns/aliases.md create mode 100644 docs/archive/current/packages/support/concerns/booting.md create mode 100644 docs/archive/current/packages/support/concerns/concernClass.md create mode 100644 docs/archive/current/packages/support/concerns/conflictResolution.md create mode 100644 docs/archive/current/packages/support/concerns/edgeCases.md create mode 100644 docs/archive/current/packages/support/concerns/hooks.md create mode 100644 docs/archive/current/packages/support/concerns/jsdoc.md create mode 100644 docs/archive/current/packages/support/concerns/prerequisites.md create mode 100644 docs/archive/current/packages/support/concerns/usage.md diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index be42a205..93955b40 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -50,6 +50,22 @@ export default PagesCollection.make('v0.x', '/v0x', [ 'packages/support/arrays/merge', ] }, + { + text: 'Concerns', + collapsible: true, + children: [ + 'packages/support/concerns/', + 'packages/support/concerns/prerequisites', + 'packages/support/concerns/concernClass', + 'packages/support/concerns/usage', + 'packages/support/concerns/aliases', + 'packages/support/concerns/conflictResolution', + 'packages/support/concerns/booting', + 'packages/support/concerns/hooks', + 'packages/support/concerns/edgeCases', + 'packages/support/concerns/jsdoc', + ] + }, { text: 'Exceptions', collapsible: true, diff --git a/docs/archive/current/packages/support/concerns/README.md b/docs/archive/current/packages/support/concerns/README.md new file mode 100644 index 00000000..5ff693f6 --- /dev/null +++ b/docs/archive/current/packages/support/concerns/README.md @@ -0,0 +1,46 @@ +--- +title: About Concerns +description: Alternative mixin utility. +sidebarDepth: 0 +--- + +# About Concerns + +Inspired by PHP's [Traits](https://www.php.net/manual/en/language.oop5.traits.php), traditional [mixins](https://javascript.info/mixins), and a few concepts from [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection), +the `@aedart/support/concerns` submodule offers an alternative approach to reducing some of the limitations of [single inheritance](https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)#Types). + +In this context, a "concern" is a class that can be injected into a target class, by means of the `use()` class decorator. +The public properties and methods of the concern class are then _"aliased"_ into the target class' prototype. In other words, +"proxy" properties and methods are defined in the target class. They forward any interaction to the original properties and methods +in the concern class instance. + +## Example + +```js +import { use, AbstractConcern } from "@aedart/support/concerns"; + +// A concern class... +class ConsolePrinter extends AbstractConcern { + print(message) { + console.log(message); + } +} + +// Taget class that uses a concern... +@use(ConsolePrinter) +class Person { + + sayHi(name) { + // Call method in concern + this.print(`Hi ${name}`); + } +} + +// Later in your application... +const person = new Person(); +person.sayHi('Atrid'); // Hi Astrid + +person.print('Ho ho ho...'); // Ho ho ho... +``` + +See also ["Real" mixins](../mixins/README.md) as an alternative. \ No newline at end of file diff --git a/docs/archive/current/packages/support/concerns/aliases.md b/docs/archive/current/packages/support/concerns/aliases.md new file mode 100644 index 00000000..32d5b09f --- /dev/null +++ b/docs/archive/current/packages/support/concerns/aliases.md @@ -0,0 +1,94 @@ +--- +title: Aliases +description: What are aliases +sidebarDepth: 0 +--- + +# Aliases + +In this context, an "alias" is a proxy property or method inside a target class. +It is responsible for forwarding interaction to the original property or method, inside the concern class instance. +Aliases are created automatically by the `use()` class decorator. + +[[TOC]] + +## Properties & Methods + +When injecting a concern into a target class, the concern's public properties and methods are defined as "aliases" +(_aka. proxy properties or methods_), in the target class' prototype (_see [`PROVIDES` symbol](./concernClass.md#customise-alias-members) for additional details_). + +Consider the following example: + +```js +import { use, AbstractConcern } from "@aedart/support/concerns"; + +class Levels extends AbstractConcern { + get level() { /* ...not shown */ } + set level(value) { /* ...not shown */ } + clear() { /* ...not shown */ } +} + +@use(Levels) +class Recorder {} +``` + +The aliasing mechanism will transform the target class into something that _**very roughly**_ corresponds to this: + +```js +import { + use, + CONCERNS, + AbstractConcern +} from "@aedart/support/concerns"; + +class Levels extends AbstractConcern { + get level() { /* ...not shown */ } + set level(value) { /* ...not shown */ } + clear(level) { /* ...not shown */ } +} + +class Recorder { + // ...private concerns container not shown... + + // get level "alias" + get level() { + return this[CONCERNS].get(Levels)['level']; + } + + // set level "alias" + set level(value) { + this[CONCERNS].get(Levels)['level'] = value; + } + + // method clear "alias" + clear(...args) { + return this[CONCERNS].get(Levels)['clear'](...args); + } +} +``` + +See [Manual interaction](./usage.md#manual-interaction) and [Conflict Resolution](./conflictResolution.md) for additional details. + +## If property or method already exists + +When a property or method from a concern already exists in the target class' prototype chain¹, then **NO Alias** is +defined. Said differently, the `use()` class decorator does **NOT** overwrite a target class' properties or methods. + +```js +class Label extends AbstractConcern { + get name() { /* ...not shown.. */ } + set name(v) { /* ...not shown.. */ } +} + +@use(Label) // Label's "name" property is NOT aliased +class Battery { + + // Battery's get/set "name" remains untouched by concern + get name() { /* ...not shown.. */ } + set name(v) { /* ...not shown.. */ } +} +``` + +¹: _Inherited properties and methods are also respected._ + +See [Conflict Resolution](./conflictResolution.md) for additional details. \ No newline at end of file diff --git a/docs/archive/current/packages/support/concerns/booting.md b/docs/archive/current/packages/support/concerns/booting.md new file mode 100644 index 00000000..6dddeb63 --- /dev/null +++ b/docs/archive/current/packages/support/concerns/booting.md @@ -0,0 +1,93 @@ +--- +title: Booting +description: How concerns are booted +sidebarDepth: 0 +--- + +# Booting + +By default, a concern class is _ONLY_ instantiated when you interact with its properties or methods, which have been "aliased" +into a target class (_aka. [lazy booting](https://en.wikipedia.org/wiki/Lazy_initialization)_). + +```js +class ContactsApi extends AbstractConcern { + get users() { /* ...not shown here... */} +} + +@use(ContactsApi) // Concern is NOT instantiated +class UsersRegistry {} + +const users = (new UsersRegistry()).users; // Concern is instantiated +``` + +## Manual Booting + +You can use the `bootConcerns()` utility to manually boot concerns. +It accepts the following arguments: + +- `instance: object|Owner` - The target class instance that uses the concerns. +- `...concerns: ConcernConstructor[]` - List of concern classes to instantiate (_aka. boot_). + +```js +import { use, bootConcerns } from "@aedart/support/concerns"; + +@use( + ConcernA, + ConcernB, + ConcernC, +) +class Target { + constructor() { + bootConcerns(this, ConcernA, ConcernB); + } +} + +const instance = new Target(); // ConcernA and ConcernB are instantiated +``` + +::: warning +If you attempt to boot a concern that has already been booted, a `BootError` will be thrown! + +To determine if a concern has already been booted, use the concern container's `hasBooted()` method. + +```js +import { + getContainer, + bootConcerns +} from "@aedart/support/concerns"; + +class Record extends ApiService { + constructor() { + super(); + + if (!getContainer(this).hasBooted(ApiConnection)) { + bootConcerns(this, ApiConnection); + } + } +} +``` + +See [Manual interaction](./usage.md#manual-interaction) for details. + +::: + +### Boot All Concerns + +If you wish to boot all concerns, use the `bootAllConcerns()` utility. + +```js +import { use, bootAllConcerns } from "@aedart/support/concerns"; + +@use( + ConcernA, + ConcernB, + ConcernC, +) +class Target { + constructor() { + bootAllConcerns(this); + } +} + +const instance = new Target(); // All concerns are initialised +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/concerns/concernClass.md b/docs/archive/current/packages/support/concerns/concernClass.md new file mode 100644 index 00000000..f0b41028 --- /dev/null +++ b/docs/archive/current/packages/support/concerns/concernClass.md @@ -0,0 +1,168 @@ +--- +title: Concern Class +description: How to define a new concern class. +sidebarDepth: 0 +--- + +# Concern Class + +This chapter shows how to create a new concern class. + +[[TOC]] + +## Inherit from `AbstractConcern` + +To create a new concern class, you can inherit from the `AbstractConcern` class. + +```js +import { AbstractConcern } from "@aedart/support/concerns"; + +class MyConcern extends AbstractConcern { + + get message() { /* ...not shown */ } + set message(message) { /* ...not shown */ } + + foo() { /* ...not shown */ } + + [MY_SYMBOL]() { /* ...not shown */ } +} +``` + +By default, all the public properties and methods (_all property keys_) are made available for ["aliasing"](./aliases.md) into a target class. +To configure which members should be made available for aliasing, see [Customise "alias" members](#customise-alias-members). + +### Private Members + +::: warning Note +Private methods and properties are **NEVER** "aliased" into a target class. +::: + +### Static Members + +::: warning Note +Static methods and properties are **NOT** "aliased" into target class. + +_At the moment, such a feature is not supported by the concerns' mechanism._ +_This may become available in the future, but presently you SHOULD NOT rely on static members becoming available for aliasing._ +::: + +### Transpilers + +::: warning Caution +Some transpilers, like [Babel](https://babeljs.io/) and [TypeScript](https://www.typescriptlang.org/), automatically move property declarations into the class' `constructor`. +For instance: + +```js +class A { + foo = 'bar'; +} +``` + +becomes like the following, after it has been transpiled: + +```js +class A { + constructor() { + this.foo = 'bar'; + } +} +``` + + +When this happens, properties cannot be "aliased". The concern mechanisms relies on the class' prototype for reading +what properties are available. To overcome such an issue, you can use [getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) and [setters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set) instead. + +```js +class A { + #foo = 'bar'; + + get foo() { + return this.#foo + } + + set foo(v) { + this.#foo = v; + } +} +``` + +::: + +## Customise "alias" members + +If you wish to customise what properties and methods _should_ be available for [aliasing](./aliases.md) when used by a +target class, overwrite the static `PROVIDES` method. + +```js +import { AbstractConcern, PROVIDES } from "@aedart/support/concerns"; + +class MyConcern extends AbstractConcern { + + get message() { /* ...not shown */ } + set message(message) { /* ...not shown */ } + foo() { /* ...not shown */ } + [MY_SYMBOL]() { /* ...not shown */ } + + static [PROVIDES]() { + // Make "message" and "foo" available for aliasing... + return [ + 'message', + 'foo' + ]; + } +} +``` + +::: warning +Even if you do customise what properties and methods are available for aliasing, the returned property keys of the +`PROVIDES` method can be overruled, via [conflict resolution](./conflictResolution.md)! + +If you truly wish to prevent certain properties or methods from being aliased into a target class, then you _should_ +declare them as [private](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties). +::: + +## Concern Owner instance + +The `concernOwner` property gives you direct access to the target class instance, in your concern. +This allows you to create interaction between the target instance and your concern, which can be usefully in any +number of situations. For instance, you can use the `concernOwner` to create a [fluent design](https://en.wikipedia.org/wiki/Fluent_interface) of your utilities. + +```js +class ConcernsPeople extends AbstractConcern { + with(value) { + // ...not shown here... + + return this.concernOwner; + } +} + +@use(ConcernsPeople) +class Invitation { + invite() { /* ...not shown here... */ } +} + +const party = new Invitation(); +party + .with('Anna') + .with('Anders') + .with('Ulla') + .with('Jimmy') + .invite(); +``` + +## Constructor + +Should you require initialisation logic, then you can overwrite the `constructor` in your concern class. +A concern's constructor is only given the target class instance as argument. + +_See [booting](./booting.md) for additional information._ + +```js +class Recording extends AbstractConcern { + constructor(owner) { + super(owner); + + // ...perform your initialisation here... + } +} +``` diff --git a/docs/archive/current/packages/support/concerns/conflictResolution.md b/docs/archive/current/packages/support/concerns/conflictResolution.md new file mode 100644 index 00000000..947583af --- /dev/null +++ b/docs/archive/current/packages/support/concerns/conflictResolution.md @@ -0,0 +1,111 @@ +--- +title: Conflict Resolution +description: How to deal with naming conflicts +sidebarDepth: 0 +--- + +# Conflict Resolution + +[[TOC]] + +## Naming Conflicts + +A concern class may _**ONLY**_ occur once in a target class' prototype chain. This is a core feature of the concerns +mechanism and cannot be circumvented. However, sometimes you may find yourself in situations where +different injected concern classes define the same property or method name. When this happens an `AliasConflictError` +is thrown. + +```js +class Label extends AbstractConcern { + get name() { /* ...not shown.. */ } + set name(v) { /* ...not shown.. */ } +} + +class Category extends AbstractConcern { + get name() { /* ...not shown.. */ } + set name(v) { /* ...not shown.. */ } +} + +@use( + Label, + Category // AliasConflictError: Alias "name" for property ... +) +class Battery {} +``` + +## Resolve Naming Conflicts + +To resolve the previous shown naming conflict, you can specify custom "aliases" when +injecting a concern class, via an injection configuration object. + +```js +// ... Label and Category concerns not shown ... + +@use( + Label, + { + concern: Category, + aliases: { + 'name': 'category' // Alias Category's "name" property as "category" + } + } +) +class Battery {} + +const instance = new Battery(); +instance.name = 'AAA'; +instance.category = 'Rechargeable'; +``` + +The `aliases` option is key-value record, where; +- key = property key in the concern class. +- value = property key (_alias_) to define in the target class. + +## Prevent Aliases + +To prevent a concern class from defining any aliases inside a target class, set the `allowAliases` option to `false`. + +```js +import { getConcernsContainer } from "@aedart/support/concerns"; + +@use( + Label, + { + concern: Category, + allowAliases: false // Category's "name" is NOT aliased in target + } +) +class Battery {} + +const instance = new Battery(); +instance.name = 'AA'; + +// Interact with Category concern to set "name" +getConcernsContainer(instance).get(Category).name = 'Rechargeable'; +``` + +## Shorthand Configuration + +You can also use a shorthand version to specify a concern injection configuration, via an array. +The first array value must always be the concern class that must be injected. +The second value can either be an `aliases` object, or boolean value for setting the `allowAliases` option. + +```js +@use( + Label, + [Category, { + 'name': 'category' + }] +) +class Battery {} +``` + +And to prevent a concern from defining aliases in a target: + +```js +@use( + Label, + [Category, false] +) +class Battery {} +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/concerns/edgeCases.md b/docs/archive/current/packages/support/concerns/edgeCases.md new file mode 100644 index 00000000..21a9d06c --- /dev/null +++ b/docs/archive/current/packages/support/concerns/edgeCases.md @@ -0,0 +1,177 @@ +--- +title: Edge Cases +description: A few edge cases when making or using concerns +sidebarDepth: 0 +--- + +# Edge Cases + +[[TOC]] + +## Getter & Setter declared in different concerns + +It is not possible to define a property's getter and setter methods in separate concerns, and thereafter use them in +a target class. Despite serving different purposes, the getter and setter share the same property name and are therefore +treated as being one and the same property key. The following example will therefore always lead to an +`AliasConflictError` being thrown. + +```js +import { AbstractConcern, use } from "@aedart/support/concerns"; + +class A extends AbstractConcern { + get title() { /* ...not shown.. */ } +} + +class B extends AbstractConcern { + set title(value) { /* ...not shown.. */ } +} + +@use( + A, + B // AliasConflictError - "title" property from A! +) +class Person {} +``` + +You can resolve the above shown issue via a [custom alias](./conflictResolution.md#resolve-naming-conflicts). But, it is +advisable to design your concerns such that the offer appropriate getter and setter methods for a property, in one and +the same concern - _if you intend for such a property to be readable and writable._ + +## Inheritance vs. Concern members + +The concerns mechanism will never overwrite existing methods or properties inside a target class - not even when those +methods or properties are inherited from a parent class. + +```js +import { AbstractConcern, use } from "@aedart/support/concerns"; + +class Connection extends AbstractConcern { + driver() { + return 'special'; + } +} + +class Api { + driver() { + return 'default'; + } +} + +@user(Connection) // driver() is NOT aliased - method inherited from Api class! +class SailBoat extends Api {} + +const instance = new SailBoat(); +instance.driver(); // default +``` + +The only way to resolve the above shown issue, is by making use of a [custom alias](./conflictResolution.md#resolve-naming-conflicts) +and manually overwrite the inherited method. E.g. + +```js +import { AbstractConcern, use } from "@aedart/support/concerns"; + +class Connection extends AbstractConcern { + driver() { + return 'special'; + } +} + +class Api { + driver() { + return 'default'; + } +} + +@user( + [Connection, { + 'driver': 'specialDriver' // alias "driver" as "specialDriver" + }] +) +class SailBoat extends Api { + + // Overwrite inherited method + driver() { + // Invoke the "specialDriver"... + return this.specialDriver(); + } +} + +const instance = new SailBoat(); +instance.driver(); // special +``` + +## Concerns using other concerns + +A concern can use other concerns classes. However, depending on your complexity, doing so may impact performance. +Consider the following example: + +```js +import { AbstractConcern, use } from "@aedart/support/concerns"; + +class Ping extends AbstractConcern { + ping() { + return 'ping'; + } +} + +@use(Ping) +class Pong extends AbstractConcern { + pong() { + return 'pong'; + } +} + +@use(Pong) +class Game {} + +const instance = new Game(); + +instance.ping(); // ping +instance.pong(); // pong +``` + +In the above shown example, whenever the `ping()` method is invoked, the call stack will be similar to the following: + +``` +Game (instance).ping() -> Pong (instance).ping() -> Ping (instance).ping() + +("->" represents concerns container instance) +``` + +In some isolated cases, this might be acceptable for you. Nevertheless, if your application makes heavy use of concerns +using other concerns, then your application's overall performance could suffer. You should consider merging multiple +concern classes into a single class, if it is reasonable and possible. Alternatively, you can also consider extending +existing concern classes. For instance: + +```js +import { AbstractConcern, use } from "@aedart/support/concerns"; + +class Ping extends AbstractConcern { + ping() { + return 'ping'; + } +} + +// Extend Ping concern... +class Pong extends Ping { + pong() { + return 'pong'; + } +} + +@use(Pong) +class Game {} + +const instance = new Game(); + +instance.ping(); // ping +instance.pong(); // pong +``` + +Now, whenever the `ping()` method is invoked, the call stack is slightly reduced: + +``` +Game (instance).ping() -> Pong (instance).ping() + +("->" represents concerns container instance) +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/concerns/hooks.md b/docs/archive/current/packages/support/concerns/hooks.md new file mode 100644 index 00000000..8d5dc02a --- /dev/null +++ b/docs/archive/current/packages/support/concerns/hooks.md @@ -0,0 +1,62 @@ +--- +title: Hooks +description: How to use registration hooks +sidebarDepth: 0 +--- + +# Hooks + +Concerns offer a few hook methods. These can be used to perform advanced setup or initialisation logic. + +[[TOC]] + +## `BEFORE` Registration + +To perform pre-registration logic, use the static `BEFORE` method in your concern class. +This hook method is invoked before the concern container and aliases are defined in the target class. + +The method accepts the following arguments: + +- `target: UsesConcerns` - the target class (_class constructor!_). + +```js +import { BEFORE } from "@aedart/contracts/support/concerns"; +import { AbstractConcern } from "@aedart/support/concerns"; +import { isSubclass } from '@aedart/support/reflections'; + +import { JobHandler } from '@acme/jobs'; + +class RecordsJobs extends AbstractConcern { + + static [BEFORE](target) { + // E.g. prevent this concern from being used by all kinds of targets... + if (!isSubclass(target, JobHandler)) { + throw new TypeError('RecordsJobs can only be used by JobHandler'); + } + } +} +``` + +## `AFTER` Registration + +To perform post-registration logic, use the static `AFTER` method in your concern class. +This method is invoked after the concern container and aliases have been defined in target's prototype. + +The method accepts the following arguments: + +- `target: UsesConcerns` - the target class (_class constructor!_). + +```js +import { AFTER } from "@aedart/contracts/support/concerns"; +import { AbstractConcern } from "@aedart/support/concerns"; + +import { ApiConnection } from '@acme/api'; + +class RecordsJobs extends AbstractConcern { + + static [AFTER](target) { + // E.g. init or setup static resources... + ApiConnection.init(); + } +} +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/concerns/jsdoc.md b/docs/archive/current/packages/support/concerns/jsdoc.md new file mode 100644 index 00000000..1546aa5a --- /dev/null +++ b/docs/archive/current/packages/support/concerns/jsdoc.md @@ -0,0 +1,211 @@ +--- +title: JSDoc +description: A wat to document what concerns a targer class uses. +sidebarDepth: 0 +--- + +# JSDoc + +Most modern IDEs support [JSDoc](https://jsdoc.app/). They can improve your coding experience via code suggestions, +highlights and other features. In this chapter, you will find a few ways that you can document your concerns and +target class, via JSDoc. + +[[TOC]] + +::: tip Help wanted! +_If you are an expert in JSDoc, then you are most welcome to help improve this chapter. +Please see the [contribution guide](../../../contribution-guide.md) for details on how you can contribute._ +::: + +## `@mixin` and `@mixes` + +Possibly the easiest way to document your concern and target class that uses the concern, is via the +[`@mixin` and `@mixes`](https://jsdoc.app/tags-mixes) tags. + +**Downside**: _Documenting "aliases" (known as virtual fields in the context of JSDoc) is not possible via `@mixin` and `@mixes`. +You can describe an alias via [`@function`](https://jsdoc.app/tags-function) or [`@member`](https://jsdoc.app/tags-member) tag._ + +```js +import { use, AbstractConcern } from "@aedart/support/concerns"; + +/** + * @mixin + * @extends AbstractConcern + */ +class Shield extends AbstractConcern { + + /** + * Returns the armor level + * + * @returns {number} + */ + get armor() { + return 8; + } + + /** + * Throw shield towards a target + * + * @param {object} target + * + * @returns {number} Damage given to target + */ + throw(target) { + // target ignored here... + return 3; + } +} + +/** + * @mixes Shield + */ +@use([Shield, { + 'throw': 'fight' +}]) +class Monster { + + /** + * Alias for {@link Shield#throw} + * + * @function fight + * @param {object} target The target to throw at... + * @return {number} Damage taken by target + * @instance + * @memberof Monster + */ + + /** + * Do stuff... + */ + do() { + this.fight({}); + } +} +``` + +## `@property` + +Another possibility is to describe properties and methods available in a target, via the [`@property`](https://jsdoc.app/tags-property) tag. +Doing so allows you to immediately describe the "alias" name. + +**Downside**: _Properties and methods described via `@property` are listed as "static" on the target class. +Also, it is not possible to reuse existing JSDoc from your concern._ + +```js +import { use, AbstractConcern } from "@aedart/support/concerns"; + +class Armor extends AbstractConcern { + + /** + * Returns the armor level + * + * @returns {number} + */ + get level() { + return 8; + } +} + +/** + * @property {number} armor Returns the armor level + */ +@use([Armor, { + 'level': 'armor' +}]) +class Hero {} +``` + +## `@borrows` + +The [`@borrows`](https://jsdoc.app/tags-borrows) tag does also offer a possible way to describe aliases. + +**Downside**: _You are still required to use [`@member`](https://jsdoc.app/tags-member) tag to describe the actual +aliases inside your target class._ + +```js +import { use, AbstractConcern } from "@aedart/support/concerns"; + +/** + * @extends AbstractConcern + */ +class Spell extends AbstractConcern { + + /** + * Cast the spell + * + * @name cast + * + * @returns {number} Damage done + */ + cast() { + return 7; + } +} + +/** + * @borrows Spell#cast as damage + */ +@use([Spell, { + 'cast': 'damage' +}]) +class Mage { + + /** + * @function damage + * @return {number} + * @instance + * @memberof Npc + */ +} +``` + +## `@member` + +Lastly, you can use [`@member`](https://jsdoc.app/tags-member) to describe all aliases directly, without relying on the [`@borrows`](https://jsdoc.app/tags-borrows) tag. + +**Downside**: _This approach can be very cumbersome. Also, reuse of JSDoc is not possible._ + +```js +import { use, AbstractConcern } from "@aedart/support/concerns"; + +class Sword extends AbstractConcern { + + /** + * Returns amount of damage + * + * @returns {number} + */ + get slash() { + return 3; + } + + /** + * Returns the sword type + * + * @name type + * @return {string} + */ + get type() { + return 'unique'; + } +} + + +@use([Sword, { + 'slash': 'damage' +}]) +class Enemy { + + /** + * @public + * @member {number} damage Alias for {@link Sword#slash} + * @memberof Enemy + */ + + /** + * @public + * @member {string} type Alias for {@link Sword#type} + * @memberof Enemy + */ +} +``` \ No newline at end of file diff --git a/docs/archive/current/packages/support/concerns/prerequisites.md b/docs/archive/current/packages/support/concerns/prerequisites.md new file mode 100644 index 00000000..e471f2f8 --- /dev/null +++ b/docs/archive/current/packages/support/concerns/prerequisites.md @@ -0,0 +1,10 @@ +--- +title: Prerequisites +description: Prerequisites for using concerns. +sidebarDepth: 0 +--- + +# Prerequisites + +At the time of this writing, [decorators](https://github.com/tc39/proposal-decorators) are still in a proposal phase. +To use concerns, you must either use [@babel/plugin-proposal-decorators](https://babeljs.io/docs/babel-plugin-proposal-decorators), or use [TypeScript 5 decorators](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#decorators). \ No newline at end of file diff --git a/docs/archive/current/packages/support/concerns/usage.md b/docs/archive/current/packages/support/concerns/usage.md new file mode 100644 index 00000000..14508344 --- /dev/null +++ b/docs/archive/current/packages/support/concerns/usage.md @@ -0,0 +1,189 @@ +--- +title: Using Concerns +description: How to use concerns. +sidebarDepth: 0 +--- + +# How to use Concerns + +[[TOC]] + +## Using Concerns + +The [class decorator](https://github.com/tc39/proposal-decorators) `use()` is used to inject one or more concern classes into a target class. + +```js +import { use } from "@aedart/support/concerns"; + +@use( + ApiConnection, + Serialization, + Collections +) +class Flight {} +``` + +When concern classes are injected, the target class is transformed and all concerns are made available inside a private `CONCERNS` property. +See [Manual interaction](#manual-interaction) and [Aliases](./aliases.md) for additional details. + +## Inheritance + +All concerns that are used by a parent class are automatically available (_inherited_), by child classes. + +```js +@use( + ApiConnection, + Serialization, + Collections +) +class ApiService {} + +class Flight extends ApiService {} // Uses ApiConnection, Serialization, ...etc +``` + +::: warning + +A concern class may _**ONLY**_ occur once in a target class' prototype chain. +An `InjectionError` is thrown, if this is violated! + +```js +@use( + ApiConnection, + Serialization, + Collections +) +class ApiService {} + +@use(Serialization) // InjectionError +class Flight extends ApiService {} +``` + +See also [Conflict Resolution](./conflictResolution.md) for additional details. +::: + +## Manual interaction + +When concerns are injected into a target, they are defined inside a "Concerns Container", which is available in the target instance via the `CONCERNS` symbol. +Should you require to perform more advanced interaction with a concern class instance, then you can obtain a concern instance via the container's `get()` method. +It will automatically ensure to [boot](./booting.md) a concern, if not already booted. + +```js +import { + use, + CONCERNS, + AbstractConcern +} from "@aedart/support/concerns"; + +class Encryption extends AbstractConcern { + encrypt(value) { /* ...not shown... */ } +} + +@use(Encryption) +class CookieStore { + constructor() { + const container = this[CONCERNS]; + const value = container.get(Encryption).encrypt('Lorum lipsum'); + + // ...remaining not shown... + } +} +``` + +You can achieve the same result by using the `getContainer()` utility method. + +```js +import { use, getContainer } from "@aedart/support/concerns"; + +// ...Encryption concern not shown... + +@use(Encryption) +class CookieStore { + constructor() { + const value = getContainer(this) + .get(Encryption) + .encrypt('Lorum lipsum'); + + // ...remaining not shown... + } +} +``` + +::: details CONCERNS symbol, getContainer(), and getConcernsContainer() + +There are 3 ways to obtain the concerns container instance: + +**A) `CONCERNS` symbol** + +Inside your target class, if you know that concerns are used (_if target is a "concern owner"_), +then you can use the `CONCERNS` symbol to gain access to the container. + +```js +import { CONCERNS } from "@aedart/support/concerns"; + +// Inside your target class... +const container = this[CONCERNS]; +``` + +**B) `getContainer()`** + +`getContainer()` is essentially a just a wrapper for: `return this[CONCERNS]`. + +```js +import { getContainer } from "@aedart/support/concerns"; + +// Inside your target class... +const container = getContainer(this); +``` + +**C) `getConcernsContainer()`** + +The `getConcernsContainer()` achieves the same result as the previous shown methods. However, it does perform a +check of the provided target instance, which ensures that it is a "concern owner". +If the target does not pass this test, then a `TypeError` is thrown. +This might can be useful in situations when you might now know if the target is a concern owner, e.g. when situated +in a child class or outside a target class. + +```js +import { getConcernsContainer } from "@aedart/support/concerns"; + +// Inside your target class... +const container = getConcernsContainer(this); +``` +::: + +### Determine if target uses concerns + +To determine if a target uses one or more concerns, use the `usesConcerns()` method. +It accepts the following arguments: + +- `instance: object|Owner` - The target class instance. +- `...concerns: ConcernConstructor[]` - Concern classes to test for. + +```js +import { + use, + AbstractConcern, + usesConcerns +} from "@aedart/support/concerns"; + +class A extends AbstractConcern {} +class B extends AbstractConcern {} +class C extends AbstractConcern {} + +@use( + A, + B +) +class Game {} + +const instance = new Game(); + +usesConcerns(instance, A); // true +usesConcerns(instance, B); // true +usesConcerns(instance, A, B); // true + +usesConcerns(instance, C); // false +usesConcerns(instance, A, C); // false +usesConcerns(instance, B, C); // false +usesConcerns(instance, A, B, C); // false +``` \ No newline at end of file From db6f1e144eddb1a7e1f90baf186e3fbcb760a410 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 5 Mar 2024 14:57:55 +0100 Subject: [PATCH 421/424] Add link to concerns as an alternative --- docs/archive/current/packages/support/mixins/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/archive/current/packages/support/mixins/README.md b/docs/archive/current/packages/support/mixins/README.md index 9d3892fa..26b3be95 100644 --- a/docs/archive/current/packages/support/mixins/README.md +++ b/docs/archive/current/packages/support/mixins/README.md @@ -38,4 +38,6 @@ const item = new Item(); item.name = 'My Item'; console.log(item.name); // My Item -``` \ No newline at end of file +``` + +See also [Concerns](../concerns/README.md) as an alternative. \ No newline at end of file From acee83cb83e4101454b124227445fea674823fb0 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 5 Mar 2024 14:58:42 +0100 Subject: [PATCH 422/424] Add available since badge --- docs/archive/current/packages/support/concerns/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/archive/current/packages/support/concerns/README.md b/docs/archive/current/packages/support/concerns/README.md index 5ff693f6..b8a7ee8d 100644 --- a/docs/archive/current/packages/support/concerns/README.md +++ b/docs/archive/current/packages/support/concerns/README.md @@ -4,7 +4,7 @@ description: Alternative mixin utility. sidebarDepth: 0 --- -# About Concerns +# About Concerns Inspired by PHP's [Traits](https://www.php.net/manual/en/language.oop5.traits.php), traditional [mixins](https://javascript.info/mixins), and a few concepts from [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection), the `@aedart/support/concerns` submodule offers an alternative approach to reducing some of the limitations of [single inheritance](https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)#Types). From 75985115fec6222f9d52b58d7bcccd25bedc318b Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 5 Mar 2024 15:06:42 +0100 Subject: [PATCH 423/424] Highlight concerns --- docs/archive/current/README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/archive/current/README.md b/docs/archive/current/README.md index e86f63f4..d57681aa 100644 --- a/docs/archive/current/README.md +++ b/docs/archive/current/README.md @@ -29,6 +29,31 @@ _TBD: "To be decided"._ ## `v0.x` Highlights +### Concerns + +Intended as an alternative to mixins, the [Concerns](./packages/support/concerns/README.md) submodule offers a different +way to overcome some of the limitations of single inheritance. + +```js +import { use, AbstractConcern } from "@aedart/support/concerns"; + +// A concern class... +class Role extends AbstractConcern { + addRole(name) { + /* ...not shown... */ + } +} + +// Use concern in target class... +@use(Role) +class User {} + +// Later in your application... +const user = new User(); +user.addRole('maintainer'); +user.addRole('supporter'); +``` + ### Merge Objects [merge](./packages/support/objects/merge.md) utility, using [deep copy](https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy). From 086f951da8c26414a60947237dbdeaac519b969c Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 5 Mar 2024 15:08:09 +0100 Subject: [PATCH 424/424] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d21f064a..2efaa880 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Concerns - an alternative to mixins, in `@aedart/contracts/support/concerns`. * `@aedart/contracts/support/exceptions` and `@aedart/support/exceptions` submodules. * `@aedart/contracts/support/objects` submodule. * `@aedart/contracts/support/arrays` and `@aedart/support/arrays` submodules.