From 6b06888cbb8a25fc86a65a7f21c4dc044594a92e Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 14:56:06 +0100 Subject: [PATCH 001/224] Init @aedart/contracts/container submodule --- aliases.js | 1 + packages/contracts/package.json | 5 +++++ packages/contracts/rollup.config.mjs | 1 + packages/contracts/src/container/index.ts | 6 ++++++ 4 files changed, 13 insertions(+) create mode 100644 packages/contracts/src/container/index.ts diff --git a/aliases.js b/aliases.js index b5f9cc7b..ca8c876a 100644 --- a/aliases.js +++ b/aliases.js @@ -19,6 +19,7 @@ module.exports = { alias: { // contracts + '@aedart/contracts/container': path.resolve(__dirname, './packages/contracts/container'), '@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'), diff --git a/packages/contracts/package.json b/packages/contracts/package.json index d6cc2c6a..76d18e93 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -31,6 +31,11 @@ "import": "./dist/esm/contracts.js", "require": "./dist/cjs/contracts.cjs" }, + "./container": { + "types": "./dist/types/container.d.ts", + "import": "./dist/esm/container.js", + "require": "./dist/cjs/container.cjs" + }, "./support": { "types": "./dist/types/support.d.ts", "import": "./dist/esm/support.js", diff --git a/packages/contracts/rollup.config.mjs b/packages/contracts/rollup.config.mjs index 066e3fd3..c58efd45 100644 --- a/packages/contracts/rollup.config.mjs +++ b/packages/contracts/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/container', '@aedart/contracts/support', '@aedart/contracts/support/arrays', '@aedart/contracts/support/concerns', diff --git a/packages/contracts/src/container/index.ts b/packages/contracts/src/container/index.ts new file mode 100644 index 00000000..95d836fa --- /dev/null +++ b/packages/contracts/src/container/index.ts @@ -0,0 +1,6 @@ +/** + * Container identifier + * + * @type {Symbol} + */ +export const CONTAINER: unique symbol = Symbol('@aedart/contracts/container'); \ No newline at end of file From 2cc865923dede9903be00aaa0c17b4704b614153 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 14:59:50 +0100 Subject: [PATCH 002/224] Init @aedart/support/container submodule --- packages/support/package.json | 5 +++++ packages/support/rollup.config.mjs | 1 + packages/support/src/container/index.ts | 4 ++++ 3 files changed, 10 insertions(+) create mode 100644 packages/support/src/container/index.ts diff --git a/packages/support/package.json b/packages/support/package.json index 13e02748..d826e33f 100644 --- a/packages/support/package.json +++ b/packages/support/package.json @@ -38,6 +38,11 @@ "import": "./dist/esm/concerns.js", "require": "./dist/cjs/concerns.cjs" }, + "./container": { + "types": "./dist/types/container.d.ts", + "import": "./dist/esm/container.js", + "require": "./dist/cjs/container.cjs" + }, "./exceptions": { "types": "./dist/types/exceptions.d.ts", "import": "./dist/esm/exceptions.js", diff --git a/packages/support/rollup.config.mjs b/packages/support/rollup.config.mjs index 299cbe5b..75707c34 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', + '@aedart/contracts/container', '@aedart/contracts/support', '@aedart/contracts/support/arrays', '@aedart/contracts/support/concerns', diff --git a/packages/support/src/container/index.ts b/packages/support/src/container/index.ts new file mode 100644 index 00000000..5ca9a138 --- /dev/null +++ b/packages/support/src/container/index.ts @@ -0,0 +1,4 @@ +/** + * TODO: Replace this + */ +export const TMP = 'TODO' \ No newline at end of file From fa5750ec002b06a1b06b123cb32208a803fd758e Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 15:11:23 +0100 Subject: [PATCH 003/224] Init @aedart/container package --- packages/container/LICENSE | 11 +++++++ packages/container/NOTICE | 13 ++++++++ packages/container/README.md | 15 +++++++++ packages/container/package.json | 47 ++++++++++++++++++++++++++++ packages/container/rollup.config.mjs | 15 +++++++++ packages/container/src/index.ts | 6 ++++ packages/container/tsconfig.json | 12 +++++++ 7 files changed, 119 insertions(+) create mode 100644 packages/container/LICENSE create mode 100644 packages/container/NOTICE create mode 100644 packages/container/README.md create mode 100644 packages/container/package.json create mode 100644 packages/container/rollup.config.mjs create mode 100644 packages/container/src/index.ts create mode 100644 packages/container/tsconfig.json diff --git a/packages/container/LICENSE b/packages/container/LICENSE new file mode 100644 index 00000000..912936bc --- /dev/null +++ b/packages/container/LICENSE @@ -0,0 +1,11 @@ +Copyright (c) 2023-2024 Alin Eugen Deac . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/container/NOTICE b/packages/container/NOTICE new file mode 100644 index 00000000..50065db0 --- /dev/null +++ b/packages/container/NOTICE @@ -0,0 +1,13 @@ +NOTICES AND INFORMATION +Please do not translate or Localize. + +Parts of the herein provided software are considered an "adaptation", or "derivative work", of 3rd party software. +Below you will find general information about which parts are affected, or where you may find additional information +such, along with original license(s), terms and conditions as provided by the 3rd party software. + +3rd party software that are included as dependencies by this software is NOT covered by this NOTICE file, unless +explicitly required by 3rd party software license(s). You can find original license(s), terms and +conditions of the included 3rd party software, in the directory where it has been installed, e.g. inside your +"node_modules" directory. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/container/README.md b/packages/container/README.md new file mode 100644 index 00000000..3fe7f648 --- /dev/null +++ b/packages/container/README.md @@ -0,0 +1,15 @@ +# Service Container + +TODO: ... + +# Official Documentation + +Please read the [official documentation](https://aedart.github.io/ion/) for additional information. + +## Versioning + +This package follows [Semantic Versioning 2.0.0](http://semver.org/) + +## License + +[BSD-3-Clause](http://spdx.org/licenses/BSD-3-Clause), please read the [`LICENSE`](./LICENSE) file included in this project. diff --git a/packages/container/package.json b/packages/container/package.json new file mode 100644 index 00000000..70d23f7a --- /dev/null +++ b/packages/container/package.json @@ -0,0 +1,47 @@ +{ + "name": "@aedart/container", + "version": "0.10.0", + "description": "Service Container", + "keywords": [ + "Service Container", + "Dependency Injection", + "Inverse of Control", + "IoC" + ], + "author": "Alin Eugen Deac ", + "license": "BSD-3-Clause", + "homepage": "https://aedart.github.io/ion/", + "bugs": { + "url": "https://github.com/aedart/ion/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/aedart/ion.git", + "directory": "packages/container" + }, + "private": false, + "publishConfig": { + "access": "public" + }, + "type": "module", + "exports": { + ".": { + "types": "./dist/types/container.d.ts", + "import": "./dist/esm/container.js", + "require": "./dist/cjs/container.cjs" + } + }, + "files": [ + "dist", + "!dist/**/*.map" + ], + "peerDependencies": { + "@aedart/contracts": "^0.10.0", + "@aedart/support": "^0.10.0" + }, + "scripts": { + "compile": "rollup -c", + "watch": "rollup -c -w" + }, + "sideEffects": false +} diff --git a/packages/container/rollup.config.mjs b/packages/container/rollup.config.mjs new file mode 100644 index 00000000..c58efd45 --- /dev/null +++ b/packages/container/rollup.config.mjs @@ -0,0 +1,15 @@ +import { createConfig } from '../../shared/rollup.config.mjs'; + +export default createConfig({ + baseDir: new URL('.', import.meta.url), + external: [ + '@aedart/contracts/container', + '@aedart/contracts/support', + '@aedart/contracts/support/arrays', + '@aedart/contracts/support/concerns', + '@aedart/contracts/support/exceptions', + '@aedart/contracts/support/meta', + '@aedart/contracts/support/mixins', + '@aedart/contracts/support/reflections', + ] +}); diff --git a/packages/container/src/index.ts b/packages/container/src/index.ts new file mode 100644 index 00000000..02b19635 --- /dev/null +++ b/packages/container/src/index.ts @@ -0,0 +1,6 @@ +/** + * Service Container identifier + * + * @type {Symbol} + */ +export const SERVICE_CONTAINER: unique symbol = Symbol('@aedart/container'); \ No newline at end of file diff --git a/packages/container/tsconfig.json b/packages/container/tsconfig.json new file mode 100644 index 00000000..27be036a --- /dev/null +++ b/packages/container/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../shared/tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "declarationDir": "./dist/tmp", + "composite": true + }, + "include": [ + "./src/**/*" + ], + "references": [] +} From ae2511b55bdfbbaff6966e9815a2a41fb049c140 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 15:13:13 +0100 Subject: [PATCH 004/224] Add path alias to container package source --- aliases.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aliases.js b/aliases.js index ca8c876a..74676665 100644 --- a/aliases.js +++ b/aliases.js @@ -18,6 +18,9 @@ module.exports = { // conditionNames: ['require', 'import'], alias: { + // container + '@aedart/container': path.resolve(__dirname, './packages/container/src'), + // contracts '@aedart/contracts/container': path.resolve(__dirname, './packages/contracts/container'), '@aedart/contracts/support/arrays': path.resolve(__dirname, './packages/contracts/support/arrays'), From 3a0f90a4b88efa7b4afa98f5f4e8b72874034843 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 7 Mar 2024 15:13:29 +0100 Subject: [PATCH 005/224] Add container path and reference --- tsconfig.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tsconfig.json b/tsconfig.json index 6eac5521..e1f3f889 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,6 +40,9 @@ "baseUrl": ".", /* Specify the base directory to resolve non-relative module names. */ "paths": { + "@aedart/container/*": [ + "./packages/container/src/*" + ], "@aedart/contracts/*": [ "./packages/contracts/src/*" ], @@ -145,6 +148,9 @@ "node_modules" ], "references": [ + { + "path": "./packages/container" + }, { "path": "./packages/contracts" }, From 06dc228f1f15ef8c2b3db110861571c14b1df781 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 8 Mar 2024 08:22:51 +0100 Subject: [PATCH 006/224] Add binding identifier type alias --- packages/contracts/src/container/index.ts | 4 +++- packages/contracts/src/container/types.ts | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 packages/contracts/src/container/types.ts diff --git a/packages/contracts/src/container/index.ts b/packages/contracts/src/container/index.ts index 95d836fa..69407934 100644 --- a/packages/contracts/src/container/index.ts +++ b/packages/contracts/src/container/index.ts @@ -3,4 +3,6 @@ * * @type {Symbol} */ -export const CONTAINER: unique symbol = Symbol('@aedart/contracts/container'); \ No newline at end of file +export const CONTAINER: unique symbol = Symbol('@aedart/contracts/container'); + +export type * from './types'; \ No newline at end of file diff --git a/packages/contracts/src/container/types.ts b/packages/contracts/src/container/types.ts new file mode 100644 index 00000000..2390412f --- /dev/null +++ b/packages/contracts/src/container/types.ts @@ -0,0 +1,7 @@ +/** + * Binding Identifier + * + * A unique identifier used for associating "concrete" items or values in + * a service container. + */ +export type Identifier = string | symbol | number | NonNullable; \ No newline at end of file From 330157b1837d11f55a057a297d65be6a76d7b9bf Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 8 Mar 2024 09:11:01 +0100 Subject: [PATCH 007/224] Add DEPENDENCIES symbol --- packages/contracts/src/container/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/contracts/src/container/index.ts b/packages/contracts/src/container/index.ts index 69407934..ecc6cb51 100644 --- a/packages/contracts/src/container/index.ts +++ b/packages/contracts/src/container/index.ts @@ -5,4 +5,14 @@ */ export const CONTAINER: unique symbol = Symbol('@aedart/contracts/container'); +/** + * Dependencies identifier + * + * Symbol is intended to be used as an identifier for when associating binding identifiers + * or "concrete" dependencies with an element, e.g. a class, class method, function,...etc. + * + * @type {symbol} + */ +export const DEPENDENCIES: unique symbol = Symbol('dependencies'); + export type * from './types'; \ No newline at end of file From b5e085fba3ed89c017342d215dd88bd96e45c331 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 8 Mar 2024 09:11:18 +0100 Subject: [PATCH 008/224] Add dependencies decorator (incomplete) --- packages/support/src/container/dependencies.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 packages/support/src/container/dependencies.ts diff --git a/packages/support/src/container/dependencies.ts b/packages/support/src/container/dependencies.ts new file mode 100644 index 00000000..149b86e0 --- /dev/null +++ b/packages/support/src/container/dependencies.ts @@ -0,0 +1,10 @@ +import type { ClassDecorator, ClassMethodDecorator } from "@aedart/contracts"; +import type { Identifier } from "@aedart/contracts/container"; +import { DEPENDENCIES } from "@aedart/contracts/container"; +import { targetMeta } from "@aedart/support/meta"; + +// TODO: ... INCOMPLETE... +export function dependencies(...identifiers: Identifier[]): ClassDecorator | ClassMethodDecorator +{ + return targetMeta(DEPENDENCIES, identifiers); +} \ No newline at end of file From ef5bc25df42c4f80a56fe5f5df5212e093d5eef2 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 8 Mar 2024 09:11:27 +0100 Subject: [PATCH 009/224] Export dependencies decorator --- packages/support/src/container/index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/support/src/container/index.ts b/packages/support/src/container/index.ts index 5ca9a138..6d778ac4 100644 --- a/packages/support/src/container/index.ts +++ b/packages/support/src/container/index.ts @@ -1,4 +1,2 @@ -/** - * TODO: Replace this - */ -export const TMP = 'TODO' \ No newline at end of file + +export * from "./dependencies"; \ No newline at end of file From 6fa37f95da7c4798ac8f75761a3c663b0fabaf61 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 8 Mar 2024 13:26:12 +0100 Subject: [PATCH 010/224] Fix JSDoc --- packages/support/src/meta/target/targetMeta.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/meta/target/targetMeta.ts b/packages/support/src/meta/target/targetMeta.ts index b3a77a11..380c3b61 100644 --- a/packages/support/src/meta/target/targetMeta.ts +++ b/packages/support/src/meta/target/targetMeta.ts @@ -11,7 +11,7 @@ import { getTargetMetaRepository } from "./getTargetMetaRepository"; * **Note**: _Method is intended to be used as a class or method decorator!_ * * @example - * ```ts + * ```js * class A { * @targetMeta('my-key', 'my-value') * foo() {} From c61d36fef2a061a18f867d22761ac1d853f95c27 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 8 Mar 2024 13:40:12 +0100 Subject: [PATCH 011/224] Change Target Meta Repository, add hasAny() method --- .../contracts/src/support/meta/TargetRepository.ts | 9 +++++++++ .../src/meta/target/TargetMetaRepository.ts | 14 ++++++++++++++ .../support/src/meta/target/hasAnyTargetMeta.ts | 13 +++++++++++++ packages/support/src/meta/target/index.ts | 1 + .../packages/support/meta/targetMeta.test.js | 7 ++++++- 5 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 packages/support/src/meta/target/hasAnyTargetMeta.ts diff --git a/packages/contracts/src/support/meta/TargetRepository.ts b/packages/contracts/src/support/meta/TargetRepository.ts index 6bf4f05b..6ce0a93f 100644 --- a/packages/contracts/src/support/meta/TargetRepository.ts +++ b/packages/contracts/src/support/meta/TargetRepository.ts @@ -57,6 +57,15 @@ export default interface TargetRepository */ has(target: object, key: Key): boolean; + /** + * Determine there is any metadata associated with target + * + * @param {object} target + * + * @return {boolean} + */ + hasAny(target: object): boolean; + /** * Inherit "target" meta from a base class. * diff --git a/packages/support/src/meta/target/TargetMetaRepository.ts b/packages/support/src/meta/target/TargetMetaRepository.ts index 13d22e2d..74d4b0e6 100644 --- a/packages/support/src/meta/target/TargetMetaRepository.ts +++ b/packages/support/src/meta/target/TargetMetaRepository.ts @@ -139,6 +139,20 @@ export default class TargetMetaRepository implements TargetRepository ); } + /** + * Determine there is any metadata associated with target + * + * @param {object} target + * + * @return {boolean} + */ + public hasAny(target: object): boolean + { + const address: MetaAddress | undefined = this.find(target); + + return address !== undefined && address[0]?.deref() !== undefined; + } + /** * Inherit "target" meta from a base class. * diff --git a/packages/support/src/meta/target/hasAnyTargetMeta.ts b/packages/support/src/meta/target/hasAnyTargetMeta.ts new file mode 100644 index 00000000..a8393949 --- /dev/null +++ b/packages/support/src/meta/target/hasAnyTargetMeta.ts @@ -0,0 +1,13 @@ +import { getTargetMetaRepository } from "./getTargetMetaRepository"; + +/** + * Determine there is any metadata associated with target + * + * @param {object} target + * + * @return {boolean} + */ +export function hasAnyTargetMeta(target: object): boolean +{ + return getTargetMetaRepository().hasAny(target); +} \ No newline at end of file diff --git a/packages/support/src/meta/target/index.ts b/packages/support/src/meta/target/index.ts index b381a22e..126b64a5 100644 --- a/packages/support/src/meta/target/index.ts +++ b/packages/support/src/meta/target/index.ts @@ -5,6 +5,7 @@ export { } export * from './getTargetMetaRepository'; +export * from './hasAnyTargetMeta'; export * from './getTargetMeta'; export * from './hasTargetMeta'; export * from './inheritTargetMeta'; diff --git a/tests/browser/packages/support/meta/targetMeta.test.js b/tests/browser/packages/support/meta/targetMeta.test.js index 0aef834a..c9353bd5 100644 --- a/tests/browser/packages/support/meta/targetMeta.test.js +++ b/tests/browser/packages/support/meta/targetMeta.test.js @@ -2,6 +2,7 @@ import { targetMeta, getTargetMeta, hasTargetMeta, + hasAnyTargetMeta, MetaError } from "@aedart/support/meta"; @@ -19,8 +20,12 @@ describe('@aedart/support/meta', () => { // ---------------------------------------------------------------------- // + expect(hasAnyTargetMeta(A)) + .withContext('Target does not appear to have any meta') + .toBeTrue(); + expect(hasTargetMeta(A, key)) - .withContext('Target does not appear to have meta') + .withContext('Target does not appear to meta for key') .toBeTrue(); const result = getTargetMeta(A, key); From 4a9bae70aec2d35247ef38b0c2ae1498998b0e3d Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 8 Mar 2024 13:40:18 +0100 Subject: [PATCH 012/224] Change release notes --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d2a852..83e50fe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +**Breaking** + +* Added `hasAny()` method in `TargetRepository` interface, in `@aedart/contracts/meta` + +**Non-breaking Changes** + * Root package Typescript dependency changed to `^5.4.2`. * `@typescript-eslint/eslint-plugin` upgraded to `^7.1.1`, in root package. * Decorator return types for `meta()`, `targetMeta()`, and `inheritTargetMeta()` (_continued to cause TS1270 and TS1238 errors_). [#8](https://github.com/aedart/ion/pull/8), [#9](https://github.com/aedart/ion/pull/9). From 613bcc0f9b4427e8bf74fc5eb4a305d82f2405cc Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 8 Mar 2024 14:04:31 +0100 Subject: [PATCH 013/224] Add utils for defining class and class method dependencies --- .../support/src/container/dependencies.ts | 23 ++++++++-- packages/support/src/container/dependsOn.ts | 14 ++++++ .../support/src/container/getDependencies.ts | 19 ++++++++ .../support/src/container/hasDependencies.ts | 16 +++++++ packages/support/src/container/index.ts | 6 ++- .../support/container/dependencies.test.js | 46 +++++++++++++++++++ 6 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 packages/support/src/container/dependsOn.ts create mode 100644 packages/support/src/container/getDependencies.ts create mode 100644 packages/support/src/container/hasDependencies.ts create mode 100644 tests/browser/packages/support/container/dependencies.test.js diff --git a/packages/support/src/container/dependencies.ts b/packages/support/src/container/dependencies.ts index 149b86e0..db40d807 100644 --- a/packages/support/src/container/dependencies.ts +++ b/packages/support/src/container/dependencies.ts @@ -1,10 +1,27 @@ -import type { ClassDecorator, ClassMethodDecorator } from "@aedart/contracts"; import type { Identifier } from "@aedart/contracts/container"; import { DEPENDENCIES } from "@aedart/contracts/container"; import { targetMeta } from "@aedart/support/meta"; -// TODO: ... INCOMPLETE... -export function dependencies(...identifiers: Identifier[]): ClassDecorator | ClassMethodDecorator +/** + * Define the dependencies that a target requires + * + * **Note**: _Method is intended to be used as a class or method decorator!_ + * + * @example + * ```js + * @dependencies('RockService', 'apiConnection') + * class Radio { + * + * @dependencies(RockSong) + * play(song) + * } + * ``` + * + * @param {...Identifier[]} identifiers + * + * @return {ClassDecorator | ClassMethodDecorator} + */ +export function dependencies(...identifiers: Identifier[]) { return targetMeta(DEPENDENCIES, identifiers); } \ No newline at end of file diff --git a/packages/support/src/container/dependsOn.ts b/packages/support/src/container/dependsOn.ts new file mode 100644 index 00000000..ddfa2ebd --- /dev/null +++ b/packages/support/src/container/dependsOn.ts @@ -0,0 +1,14 @@ +import type { Identifier } from "@aedart/contracts/container"; +import { dependencies } from "./dependencies"; + +/** + * Alias for [dependencies()]{@link import('@aedart/support/container').dependencies} + * + * @param {...Identifier[]} identifiers + * + * @return {ClassDecorator | ClassMethodDecorator} + */ +export function dependsOn(...identifiers: Identifier[]) +{ + return dependencies(...identifiers); +} \ No newline at end of file diff --git a/packages/support/src/container/getDependencies.ts b/packages/support/src/container/getDependencies.ts new file mode 100644 index 00000000..859ba629 --- /dev/null +++ b/packages/support/src/container/getDependencies.ts @@ -0,0 +1,19 @@ +import type { Identifier } from "@aedart/contracts/container"; +import { DEPENDENCIES } from "@aedart/contracts/container"; +import { getTargetMeta } from "@aedart/support/meta"; + + +/** + * Returns the defined dependencies for given target + * + * @param {object} target + * + * @return {Identifier[]} Empty identifiers list, if none defined for target + */ +export function getDependencies(target: object): Identifier[] +{ + return getTargetMeta< + Identifier[], + Identifier[] + >(target, DEPENDENCIES, []) as Identifier[]; +} \ No newline at end of file diff --git a/packages/support/src/container/hasDependencies.ts b/packages/support/src/container/hasDependencies.ts new file mode 100644 index 00000000..08c0995f --- /dev/null +++ b/packages/support/src/container/hasDependencies.ts @@ -0,0 +1,16 @@ +import { DEPENDENCIES } from "@aedart/contracts/container"; +import { hasTargetMeta } from "@aedart/support/meta"; + +/** + * Determine if target has dependencies defined + * + * @see dependencies + * + * @param {object} target + * + * @return {boolean} + */ +export function hasDependencies(target: object): boolean +{ + return hasTargetMeta(target, DEPENDENCIES); +} \ No newline at end of file diff --git a/packages/support/src/container/index.ts b/packages/support/src/container/index.ts index 6d778ac4..3411a627 100644 --- a/packages/support/src/container/index.ts +++ b/packages/support/src/container/index.ts @@ -1,2 +1,4 @@ - -export * from "./dependencies"; \ No newline at end of file +export * from "./dependencies"; +export * from "./dependsOn"; +export * from "./getDependencies"; +export * from "./hasDependencies"; \ No newline at end of file diff --git a/tests/browser/packages/support/container/dependencies.test.js b/tests/browser/packages/support/container/dependencies.test.js new file mode 100644 index 00000000..5428a262 --- /dev/null +++ b/tests/browser/packages/support/container/dependencies.test.js @@ -0,0 +1,46 @@ +import { dependsOn, hasDependencies, getDependencies} from "@aedart/support/container"; + +describe('@aedart/support/container', () => { + describe('@dependsOn', () => { + + it('can defined dependencies for class', () => { + const dependencies = [ 'a', 'b', 123 ]; + + @dependsOn(...dependencies) + class A {} + + // ---------------------------------------------------------------- // + + expect(hasDependencies(A)) + .withContext('No dependencies defined') + .toBeTrue(); + + expect(getDependencies(A)) + .withContext('Incorrect dependencies for target') + .toEqual(dependencies); + }); + + it('can defined dependencies for class method', () => { + const dependencies = [ 'foo', 'bar', {} ]; + + class A { + + @dependsOn(...dependencies) + index(a, b, c) {} + } + + // ---------------------------------------------------------------- // + + const instance = new A(); + const target = instance.index; + + expect(hasDependencies(target)) + .withContext('No dependencies defined') + .toBeTrue(); + + expect(getDependencies(target)) + .withContext('Incorrect dependencies for target') + .toEqual(dependencies); + }); + }); +}); \ No newline at end of file From c8cf002dfeeefe7a0fad19a923a9aa0adf21971b Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 8 Mar 2024 14:05:37 +0100 Subject: [PATCH 014/224] Change release notes --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83e50fe5..a83451d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* `DEPENDENCIES` symbol and `Identifier` type in `@aedart/contracts/container`. +* `dependsOn()`, `dependencies()`, `hasDependencies()`, and `getDependencies()`, in `@aedart/support/container`. * Add upgrade guide from v0.7.x- to v0.10.x. ### Changed From 5579444fe75d9dff55af377bcdf375acdc4043bf Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 8 Mar 2024 20:02:51 +0100 Subject: [PATCH 015/224] Update dependencies --- package-lock.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/package-lock.json b/package-lock.json index 77c5ef1f..0f166eb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,6 +77,10 @@ "node": ">=0.10.0" } }, + "node_modules/@aedart/container": { + "resolved": "packages/container", + "link": true + }, "node_modules/@aedart/contracts": { "resolved": "packages/contracts", "link": true @@ -20797,11 +20801,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/container": { + "version": "0.10.0", + "license": "BSD-3-Clause", + "peerDependencies": { + "@aedart/contracts": "^0.10.0", + "@aedart/support": "^0.10.0" + } + }, "packages/contracts": { + "name": "@aedart/contracts", "version": "0.10.0", "license": "BSD-3-Clause" }, "packages/support": { + "name": "@aedart/support", "version": "0.10.0", "license": "BSD-3-Clause", "peerDependencies": { @@ -20811,6 +20825,7 @@ } }, "packages/vuepress-utils": { + "name": "@aedart/vuepress-utils", "version": "0.10.0", "license": "BSD-3-Clause", "peerDependencies": { @@ -20824,6 +20839,7 @@ } }, "packages/xyz": { + "name": "@aedart/xyz", "version": "0.10.0", "license": "BSD-3-Clause", "peerDependencies": { From cd6e40e507611a86a8242e60888332ea24ed9473 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 8 Mar 2024 20:21:09 +0100 Subject: [PATCH 016/224] Fix typo --- packages/container/NOTICE | 2 +- packages/support/NOTICE | 4 ++-- packages/vuepress-utils/NOTICE | 4 ++-- packages/xyz/NOTICE | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/container/NOTICE b/packages/container/NOTICE index 50065db0..d638e176 100644 --- a/packages/container/NOTICE +++ b/packages/container/NOTICE @@ -3,7 +3,7 @@ Please do not translate or Localize. Parts of the herein provided software are considered an "adaptation", or "derivative work", of 3rd party software. Below you will find general information about which parts are affected, or where you may find additional information -such, along with original license(s), terms and conditions as provided by the 3rd party software. +, along with original license(s), terms and conditions as provided by the 3rd party software. 3rd party software that are included as dependencies by this software is NOT covered by this NOTICE file, unless explicitly required by 3rd party software license(s). You can find original license(s), terms and diff --git a/packages/support/NOTICE b/packages/support/NOTICE index a57656f4..1aef8b1a 100644 --- a/packages/support/NOTICE +++ b/packages/support/NOTICE @@ -2,8 +2,8 @@ NOTICES AND INFORMATION Please do not translate or Localize. Parts of the herein provided software are considered an "adaptation", or "derivative work", of 3rd party software. -Below you will find general information about which parts are affected, or where you may find additional information -such, along with original license(s), terms and conditions as provided by the 3rd party software. +Below you will find general information about which parts are affected, or where you may find additional information, +along with original license(s), terms and conditions as provided by the 3rd party software. 3rd party software that are included as dependencies by this software is NOT covered by this NOTICE file, unless explicitly required by 3rd party software license(s). You can find original license(s), terms and diff --git a/packages/vuepress-utils/NOTICE b/packages/vuepress-utils/NOTICE index 862de7b4..e41222ca 100644 --- a/packages/vuepress-utils/NOTICE +++ b/packages/vuepress-utils/NOTICE @@ -2,8 +2,8 @@ NOTICES AND INFORMATION Please do not translate or Localize. Parts of the herein provided software are considered an "adaptation", or "derivative work", of 3rd party software. -Below you will find general information about which parts are affected, or where you may find additional information -such, along with original license(s), terms and conditions as provided by the 3rd party software. +Below you will find general information about which parts are affected, or where you may find additional information, +along with original license(s), terms and conditions as provided by the 3rd party software. 3rd party software that are included as dependencies by this software is NOT covered by this NOTICE file, unless explicitly required by 3rd party software license(s). You can find original license(s), terms and diff --git a/packages/xyz/NOTICE b/packages/xyz/NOTICE index 862de7b4..e41222ca 100644 --- a/packages/xyz/NOTICE +++ b/packages/xyz/NOTICE @@ -2,8 +2,8 @@ NOTICES AND INFORMATION Please do not translate or Localize. Parts of the herein provided software are considered an "adaptation", or "derivative work", of 3rd party software. -Below you will find general information about which parts are affected, or where you may find additional information -such, along with original license(s), terms and conditions as provided by the 3rd party software. +Below you will find general information about which parts are affected, or where you may find additional information, +along with original license(s), terms and conditions as provided by the 3rd party software. 3rd party software that are included as dependencies by this software is NOT covered by this NOTICE file, unless explicitly required by 3rd party software license(s). You can find original license(s), terms and From 0e86c1781d8211c79e37cfd3e1e46b04f6bf835d Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 8 Mar 2024 20:21:15 +0100 Subject: [PATCH 017/224] Fix typo --- NOTICE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NOTICE b/NOTICE index 50a07f35..f7871245 100644 --- a/NOTICE +++ b/NOTICE @@ -2,8 +2,8 @@ NOTICES AND INFORMATION Please do not translate or Localize. Parts of the herein provided software are considered an "adaptation", or "derivative work", of 3rd party software. -Below you will find general information about which parts are affected, or where you may find additional information -such, along with original license(s), terms and conditions as provided by the 3rd party software. +Below you will find general information about which parts are affected, or where you may find additional information, +along with original license(s), terms and conditions as provided by the 3rd party software. 3rd party software that are included as dependencies by this software is NOT covered by this NOTICE file, unless explicitly required by 3rd party software license(s). You can find original license(s), terms and From 0c94e59527fe8ac8e59d68a7b6a04e82b7bb847f Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 8 Mar 2024 20:22:59 +0100 Subject: [PATCH 018/224] Add Container Exception and Not Found Exception interfaces --- .../src/container/exceptions/ContainerException.ts | 11 +++++++++++ .../src/container/exceptions/NotFoundException.ts | 11 +++++++++++ packages/contracts/src/container/exceptions/index.ts | 6 ++++++ packages/contracts/src/container/index.ts | 1 + 4 files changed, 29 insertions(+) create mode 100644 packages/contracts/src/container/exceptions/ContainerException.ts create mode 100644 packages/contracts/src/container/exceptions/NotFoundException.ts create mode 100644 packages/contracts/src/container/exceptions/index.ts diff --git a/packages/contracts/src/container/exceptions/ContainerException.ts b/packages/contracts/src/container/exceptions/ContainerException.ts new file mode 100644 index 00000000..5b48e63a --- /dev/null +++ b/packages/contracts/src/container/exceptions/ContainerException.ts @@ -0,0 +1,11 @@ +import {Throwable} from "@aedart/contracts/support/exceptions"; + +/** + * Container Exception + * + * General exception to be thrown when something went wrong inside + * a service container. Inspired by Psr's `ContainerExceptionInterface`. + * + * @see https://www.php-fig.org/psr/psr-11/#32-psrcontainercontainerexceptioninterface + */ +export default interface ContainerException extends Throwable {} \ No newline at end of file diff --git a/packages/contracts/src/container/exceptions/NotFoundException.ts b/packages/contracts/src/container/exceptions/NotFoundException.ts new file mode 100644 index 00000000..d6b2c278 --- /dev/null +++ b/packages/contracts/src/container/exceptions/NotFoundException.ts @@ -0,0 +1,11 @@ +import ContainerException from "./ContainerException"; + +/** + * Not Found Exception + * + * To be thrown when no entry was found for a given binding identifier. + * Inspired by Psr's `NotFoundExceptionInterface`. + * + * @see https://www.php-fig.org/psr/psr-11/#33-psrcontainernotfoundexceptioninterface + */ +export default interface NotFoundException extends ContainerException {} \ No newline at end of file diff --git a/packages/contracts/src/container/exceptions/index.ts b/packages/contracts/src/container/exceptions/index.ts new file mode 100644 index 00000000..2d5af743 --- /dev/null +++ b/packages/contracts/src/container/exceptions/index.ts @@ -0,0 +1,6 @@ +import ContainerException from "./ContainerException"; +import NotFoundException from "./NotFoundException"; +export { + type ContainerException, + type NotFoundException +} \ No newline at end of file diff --git a/packages/contracts/src/container/index.ts b/packages/contracts/src/container/index.ts index ecc6cb51..ab48e0dd 100644 --- a/packages/contracts/src/container/index.ts +++ b/packages/contracts/src/container/index.ts @@ -15,4 +15,5 @@ export const CONTAINER: unique symbol = Symbol('@aedart/contracts/container'); */ export const DEPENDENCIES: unique symbol = Symbol('dependencies'); +export * from './exceptions/index'; export type * from './types'; \ No newline at end of file From 59167c2c435e5ad0a4f5cd351e543766059177b6 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 8 Mar 2024 20:24:08 +0100 Subject: [PATCH 019/224] Add notice for Psr / MIT License --- packages/contracts/NOTICE | 41 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/contracts/NOTICE b/packages/contracts/NOTICE index f97cc79b..215a60c2 100644 --- a/packages/contracts/NOTICE +++ b/packages/contracts/NOTICE @@ -2,8 +2,8 @@ NOTICES AND INFORMATION Please do not translate or Localize. Parts of the herein provided software are considered an "adaptation", or "derivative work", of 3rd party software. -Below you will find general information about which parts are affected, or where you may find additional information -such, along with original license(s), terms and conditions as provided by the 3rd party software. +Below you will find general information about which parts are affected, or where you may find additional information, +along with original license(s), terms and conditions as provided by the 3rd party software. 3rd party software that are included as dependencies by this software is NOT covered by this NOTICE file, unless explicitly required by 3rd party software license(s). You can find original license(s), terms and @@ -253,4 +253,39 @@ conditions of the included 3rd party software, in the directory where it has bee distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@aedart/contracts/container (Container, ContainerException, NotFoundException) + Parts of the service container interface, along with the exception interfaces are adapted from + Psr's version thereof. + + See https://www.php-fig.org/psr/psr-11/ + + License MIT, Copyright (c) 2013-2016 container-interop + Copyright (c) 2016 PHP Framework Interoperability Group + + This part of the NOTICE file corresponds to terms and conditions set by the MIT License + ======================================================================================= + + The MIT License (MIT) + + Copyright (c) 2013-2016 container-interop + Copyright (c) 2016 PHP Framework Interoperability Group + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file From cc3c0c6c940db01548e9785b62850f4d1c0e8992 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 8 Mar 2024 20:38:58 +0100 Subject: [PATCH 020/224] Add reference to service container --- packages/contracts/NOTICE | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/contracts/NOTICE b/packages/contracts/NOTICE index 215a60c2..baf4cfe1 100644 --- a/packages/contracts/NOTICE +++ b/packages/contracts/NOTICE @@ -15,6 +15,10 @@ conditions of the included 3rd party software, in the directory where it has bee The Arrayable interface is an interface is an adaptation of Laravel's Arrayable interface. See https://github.com/laravel/framework/blob/master/src/Illuminate/Contracts/Support/Arrayable.php +@aedart/contracts/container (Container) + The service container interface is heavily inspired by Laravel's Container + + See https://github.com/laravel/framework/blob/master/src/Illuminate/Contracts/Container/Container.php License MIT, Copyright (c) Taylor Otwell. From e7c15086c217f2eabd7eaf08be6ca1976855bf08 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 8 Mar 2024 20:41:37 +0100 Subject: [PATCH 021/224] Add binding alias --- packages/contracts/src/container/types.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/container/types.ts b/packages/contracts/src/container/types.ts index 2390412f..aaad18b2 100644 --- a/packages/contracts/src/container/types.ts +++ b/packages/contracts/src/container/types.ts @@ -4,4 +4,11 @@ * A unique identifier used for associating "concrete" items or values in * a service container. */ -export type Identifier = string | symbol | number | NonNullable; \ No newline at end of file +export type Identifier = string | symbol | number | NonNullable; + +/** + * Binding Alias + * + * @see Identifier + */ +export type Alias = Identifier; \ No newline at end of file From 35d33330714355f455f6b8a08b0bdf1f798f0c6c Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 8 Mar 2024 21:13:48 +0100 Subject: [PATCH 022/224] Add FactoryCallback type alias --- packages/contracts/src/container/types.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/container/types.ts b/packages/contracts/src/container/types.ts index aaad18b2..a1b9ce01 100644 --- a/packages/contracts/src/container/types.ts +++ b/packages/contracts/src/container/types.ts @@ -1,3 +1,5 @@ +import Container from "./Container"; + /** * Binding Identifier * @@ -11,4 +13,11 @@ export type Identifier = string | symbol | number | NonNullable; * * @see Identifier */ -export type Alias = Identifier; \ No newline at end of file +export type Alias = Identifier; + +/** + * Callback that returns a resolved value + */ +export type FactoryCallback< + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +> = (container: Container, ...args: any[]) => T; \ No newline at end of file From b4d4c3889db9128526d846cd3a78ddc6a7fbfab9 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 8 Mar 2024 21:27:44 +0100 Subject: [PATCH 023/224] Add Binding Entry interface --- packages/contracts/src/container/Binding.ts | 55 +++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 packages/contracts/src/container/Binding.ts diff --git a/packages/contracts/src/container/Binding.ts b/packages/contracts/src/container/Binding.ts new file mode 100644 index 00000000..d1ea9107 --- /dev/null +++ b/packages/contracts/src/container/Binding.ts @@ -0,0 +1,55 @@ +import { Constructor } from "@aedart/contracts"; +import { FactoryCallback, Identifier } from "./types"; + +/** + * Binding Entry + * + * @template T = any + */ +export default interface Binding< + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +> { + /** + * This binding's identifier + * + * @type {Identifier} + * + * @readonly + */ + readonly identifier: Identifier; + + /** + * The bound value to be resolved by a service container + * + * @template T = any + * + * @type {FactoryCallback | Constructor} + * + * @readonly + */ + readonly value: FactoryCallback | Constructor; + + /** + * Shared state of resolved value + * + * @type {boolean} If `true`, then service container must register resolved + * value as a singleton. + * + * @readonly + */ + readonly shared: boolean; + + /** + * Determine if bound value is a {@link FactoryCallback} + * + * @returns {boolean} + */ + isFactoryCallback(): boolean; + + /** + * Determine if bound value is a {@link Constructor} + * + * @returns {boolean} + */ + isConstructor(): boolean; +} \ No newline at end of file From 6537cd323f431d4195caf10b5f2d39e9d46e093d Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 8 Mar 2024 22:22:19 +0100 Subject: [PATCH 024/224] Add Container interface (incomplete) --- packages/contracts/src/container/Container.ts | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 packages/contracts/src/container/Container.ts diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts new file mode 100644 index 00000000..c4059efd --- /dev/null +++ b/packages/contracts/src/container/Container.ts @@ -0,0 +1,200 @@ +import { Constructor } from "@aedart/contracts"; +import { + Alias, + Identifier, + FactoryCallback +} from "./types"; +import Binding from "./Binding"; + +/** + * Service Container + * + * Inspired by Psr's `ContainerInterface`, and Laravel's service `Container`. + * + * @see https://www.php-fig.org/psr/psr-11/#31-psrcontainercontainerinterface + * @see https://github.com/laravel/framework/blob/master/src/Illuminate/Contracts/Container/Container.php + */ +export default interface Container +{ + /** + * Register a binding + * + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} [concrete] + * @param {boolean} [shared=false] + * + * @returns {this} + * + * @throws {TypeError} + */ + bind(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared?: boolean): this; + + /** + * Register a binding, if none already exists for given identifier + * + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} [concrete] + * @param {boolean} [shared=false] + * + * @returns {this} + * + * @throws {TypeError} + */ + bindIf(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared?: boolean): this; + + /** + * Register a shared binding + * + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} [concrete] + * + * @returns {this} + * + * @throws {TypeError} + */ + singleton(identifier: Identifier, concrete?: FactoryCallback | Constructor): this; + + /** + * Register a shared binding, if none already exists for given identifier + * + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} [concrete] + * + * @returns {this} + * + * @throws {TypeError} + */ + singletonIf(identifier: Identifier, concrete?: FactoryCallback | Constructor): this; + + /** + * Register existing object instance as a shared binding + * + * @template T = object + * + * @param {Identifier} identifier + * @param {T} instance + * + * @returns {T} + * + * @throws {TypeError} + */ + instance< + T = object + >(identifier: Identifier, instance: T): T; + + /** + * Resolves binding value that matches given identifier and returns it + * + * @template T = any + * + * @param {Identifier} identifier + * + * @returns {T} + * + * @throws {NotFoundException} + * @throws {ContainerException} + */ + get< + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(identifier: Identifier): T; + + /** + * Determine if an entry is registered for given identifier. + * + * @param {Identifier} identifier + * + * @returns {boolean} + */ + has(identifier: Identifier): boolean; + + /** + * Alias for {@link has} + * + * @param {Identifier} identifier + * + * @returns {boolean} + */ + bound(identifier: Identifier): boolean; + + /** + * Alias identifier as a different identifier + * + * @param {Identifier} identifier + * @param {Alias} alias + * + * @returns {this} + * + * @throws {TypeError} + */ + alias(identifier: Identifier, alias: Alias): this; + + /** + * Resolves binding value that matches given identifier and returns it + * + * @template T = any + * + * @param {Identifier} identifier + * @param {any[]} [args] Eventual arguments to pass on to {@link FactoryCallback} or {@link Constructor} + * + * @returns {T} + * + * @throws {NotFoundException} + * @throws {ContainerException} + */ + make< + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(identifier: Identifier, args?: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */): T; + + /** + * Resolves binding value for identifier if one exists, or return default value + * + * @template T = any + * @template D = any + * + * @param {Identifier} identifier + * @param {any[]} [args] Eventual arguments to pass on to {@link FactoryCallback} or {@link Constructor} + * @param {D} [defaultValue] + * + * @returns {T} + * + * @throws {ContainerException} + */ + makeOrDefault< + T = any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + D = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(identifier: Identifier, args?: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */, defaultValue?: D): T | D; + + /** + * Instantiate a new instance of given concrete + * + * @template T = object + * + * @param {Constructor | Binding} concrete + * + * @returns {T} + * + * @throws {ContainerException} + */ + build< + T = object + >(concrete: Constructor | Binding): T; + + // TODO: ... + call(method: any, args: any[]): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + /** + * Forget binding and resolved instance for given identifier + * + * @param {Identifier} identifier + * + * @returns {boolean} + */ + forget(identifier: Identifier): boolean; + + /** + * Flush container of all bindings and resolved instances + * + * @returns {void} + */ + flush(): void; +} \ No newline at end of file From d4f700ea3b1c39c3068b0f6c4b43466e35620b82 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 8 Mar 2024 22:22:33 +0100 Subject: [PATCH 025/224] Export Binding Entry and Container --- packages/contracts/src/container/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/contracts/src/container/index.ts b/packages/contracts/src/container/index.ts index ab48e0dd..a27576b9 100644 --- a/packages/contracts/src/container/index.ts +++ b/packages/contracts/src/container/index.ts @@ -15,5 +15,12 @@ export const CONTAINER: unique symbol = Symbol('@aedart/contracts/container'); */ export const DEPENDENCIES: unique symbol = Symbol('dependencies'); +import Binding from "./Binding"; +import Container from "./Container"; +export { + type Binding, + type Container +} + export * from './exceptions/index'; export type * from './types'; \ No newline at end of file From 47293103cf2beb71228c38b14ecd415676ef6350 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 8 Mar 2024 22:25:53 +0100 Subject: [PATCH 026/224] Improve description of FactoryCallback type alias --- packages/contracts/src/container/types.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/container/types.ts b/packages/contracts/src/container/types.ts index a1b9ce01..8d4a6d28 100644 --- a/packages/contracts/src/container/types.ts +++ b/packages/contracts/src/container/types.ts @@ -16,8 +16,11 @@ export type Identifier = string | symbol | number | NonNullable; export type Alias = Identifier; /** - * Callback that returns a resolved value + * Factory Callback + * + * The callback is responsible for resolving a value, when a binding + * is resolved in a service container. */ export type FactoryCallback< - T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ -> = (container: Container, ...args: any[]) => T; \ No newline at end of file + Value = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +> = (container: Container, ...args: any[]) => Value; \ No newline at end of file From d7a159378037b574db439ca263c9d2829148ff58 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 8 Mar 2024 22:27:11 +0100 Subject: [PATCH 027/224] Cleanup --- packages/contracts/src/container/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/container/types.ts b/packages/contracts/src/container/types.ts index 8d4a6d28..f7514bbe 100644 --- a/packages/contracts/src/container/types.ts +++ b/packages/contracts/src/container/types.ts @@ -23,4 +23,4 @@ export type Alias = Identifier; */ export type FactoryCallback< Value = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ -> = (container: Container, ...args: any[]) => Value; \ No newline at end of file +> = (container: Container, ...args: any[]) => Value; \ No newline at end of file From bb8aea49516d1a1b6cab962193269a7edd71174e Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 8 Mar 2024 22:44:35 +0100 Subject: [PATCH 028/224] Cleanup Also, changed the D generic to undefined as default for makeOrDefault(). --- packages/contracts/src/container/Container.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index c4059efd..16f41b84 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -78,12 +78,10 @@ export default interface Container * * @throws {TypeError} */ - instance< - T = object - >(identifier: Identifier, instance: T): T; + instance(identifier: Identifier, instance: T): T; /** - * Resolves binding value that matches given identifier and returns it + * Resolves binding value that matches identifier and returns it * * @template T = any * @@ -129,7 +127,7 @@ export default interface Container alias(identifier: Identifier, alias: Alias): this; /** - * Resolves binding value that matches given identifier and returns it + * Resolves binding value that matches identifier and returns it * * @template T = any * @@ -146,10 +144,10 @@ export default interface Container >(identifier: Identifier, args?: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */): T; /** - * Resolves binding value for identifier if one exists, or return default value + * Resolves values if a binding exists for identifier, or returns a default value * * @template T = any - * @template D = any + * @template D = undefined * * @param {Identifier} identifier * @param {any[]} [args] Eventual arguments to pass on to {@link FactoryCallback} or {@link Constructor} @@ -161,7 +159,7 @@ export default interface Container */ makeOrDefault< T = any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ - D = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + D = undefined >(identifier: Identifier, args?: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */, defaultValue?: D): T | D; /** @@ -175,9 +173,7 @@ export default interface Container * * @throws {ContainerException} */ - build< - T = object - >(concrete: Constructor | Binding): T; + build(concrete: Constructor | Binding): T; // TODO: ... call(method: any, args: any[]): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ From c769df8b1fd770622fd4143e1ec366f11446c0dd Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 10:38:39 +0100 Subject: [PATCH 029/224] Add ClassMethodName and ClassMethodReference type aliases --- packages/contracts/src/types.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/types.ts b/packages/contracts/src/types.ts index b19e0e14..cf85132b 100644 --- a/packages/contracts/src/types.ts +++ b/packages/contracts/src/types.ts @@ -20,4 +20,21 @@ export type AbstractConstructor = abstract new (...args: any[]) => T /** * Constructor or Abstract Constructor type */ -export type ConstructorOrAbstractConstructor = Constructor | AbstractConstructor; \ No newline at end of file +export type ConstructorOrAbstractConstructor = Constructor | AbstractConstructor; + +/** + * Class method name + */ +export type ClassMethodName = { + [Name in keyof T]: T[Name] extends Function /* eslint-disable-line @typescript-eslint/ban-types */ + ? Name + : never; +}[keyof T]; + +/** + * Class Method Reference + * + * Array that contains either a class constructor or class instance, and the method name + * that must be processed at some point. E.g. the method to be invoked. + */ +export type ClassMethodReference = [ Constructor | T, ClassMethodName ]; \ No newline at end of file From c4b66201df6e24fa55362a2fa9ff579ef9cacb9a Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 10:38:44 +0100 Subject: [PATCH 030/224] Change release notes --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a83451d0..22039529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * `DEPENDENCIES` symbol and `Identifier` type in `@aedart/contracts/container`. -* `dependsOn()`, `dependencies()`, `hasDependencies()`, and `getDependencies()`, in `@aedart/support/container`. +* `dependsOn()`, `dependencies()`, `hasDependencies()`, and `getDependencies()`, in `@aedart/support/container`. +* `ClassMethodName` and `ClassMethodReference` type aliases in `@aedart/contracts`. * Add upgrade guide from v0.7.x- to v0.10.x. ### Changed From 4aa1a394363992d266326a24b7f1ff2e47d35373 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 12:11:49 +0100 Subject: [PATCH 031/224] Add isMethod() util --- docs/.vuepress/archive/Version0x.ts | 1 + .../packages/support/reflections/isMethod.md | 43 +++++++++++++++++ packages/support/src/reflections/isMethod.ts | 15 ++++++ .../support/reflections/isMethod.test.js | 47 +++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 docs/archive/current/packages/support/reflections/isMethod.md create mode 100644 packages/support/src/reflections/isMethod.ts create mode 100644 tests/browser/packages/support/reflections/isMethod.test.js diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index 93955b40..8acb80b2 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -144,6 +144,7 @@ export default PagesCollection.make('v0.x', '/v0x', [ 'packages/support/reflections/isConstructor', 'packages/support/reflections/isKeySafe', 'packages/support/reflections/isKeyUnsafe', + 'packages/support/reflections/isMethod', 'packages/support/reflections/isSubclass', 'packages/support/reflections/isSubclassOrLooksLike', 'packages/support/reflections/isWeakKind', diff --git a/docs/archive/current/packages/support/reflections/isMethod.md b/docs/archive/current/packages/support/reflections/isMethod.md new file mode 100644 index 00000000..8718b41a --- /dev/null +++ b/docs/archive/current/packages/support/reflections/isMethod.md @@ -0,0 +1,43 @@ +--- +title: Is Method +description: Determine if property is a method in target +sidebarDepth: 0 +--- + +# `isMethod` + +Determine if property (_name_) is a method in given target object. + +It accepts the following arguments: + +- `target: object` - The target. +- `property: PropertyKey` - Name of property. + +```js +import { isMethod } from '@aedart/support/reflections'; + +class A { + age = 23; + + #title = 'AAA'; + get title() { + return this.#title; + } + + #job = 'AAA'; + set job(v) { + this.#job = v; + } + + foo: () => { /* ...not shown... */ } +} + +const a = new A(); + +isMethod(a, 'age'); // false +isMethod(a, 'title'); // false +isMethod(a, 'job'); // false +isMethod(a, 'foo'); // true +``` + +See also [`hasMethod()`](./hasMethod.md). \ No newline at end of file diff --git a/packages/support/src/reflections/isMethod.ts b/packages/support/src/reflections/isMethod.ts new file mode 100644 index 00000000..35abcc64 --- /dev/null +++ b/packages/support/src/reflections/isMethod.ts @@ -0,0 +1,15 @@ +/** + * Determine if given property key is a method in target + * + * @param {object} target + * @param {PropertyKey} property + * + * @return {boolean} + */ +export function isMethod(target: object, property: PropertyKey): boolean +{ + return typeof target == 'object' + && target !== null + && Reflect.has(target, property) + && typeof target[property as keyof typeof target] == 'function'; +} \ No newline at end of file diff --git a/tests/browser/packages/support/reflections/isMethod.test.js b/tests/browser/packages/support/reflections/isMethod.test.js new file mode 100644 index 00000000..02bd3651 --- /dev/null +++ b/tests/browser/packages/support/reflections/isMethod.test.js @@ -0,0 +1,47 @@ +import { isMethod } from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('isMethod()', () => { + + it('can determine if property is a class method', () => { + + const MY_PROP = Symbol('@my_prop') + const MY_METHOD = Symbol('@my_method') + class A { + name = 'Olaf'; + [MY_PROP] = 23; + #title = ''; + + get age() { return 45 } // Checks the actual return type... + set title(v) { this.#title = v } + + foo() {} + [MY_METHOD]() {} + } + + const instance = new A(); + + const data = [ + { target: undefined, property: undefined, expected: false, name: 'Undefined target' }, + { target: null, property: undefined, expected: false, name: 'null target' }, + + { target: instance, property: undefined, expected: false, name: 'undefined property' }, + { target: instance, property: null, expected: false, name: 'null property' }, + + { target: instance, property: 'name', expected: false, name: 'field' }, + { target: instance, property: 'age', expected: false, name: 'getter' }, + { target: instance, property: 'title', expected: false, name: 'setter' }, + { target: instance, property: MY_PROP, expected: false, name: 'symbol field' }, + + { target: instance, property: 'foo', expected: true, name: 'method' }, + { target: instance, property: MY_METHOD, expected: true, name: 'symbol method' }, + ]; + + for (const entry of data) { + expect(isMethod(entry.target, entry.property)) + .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) + .toBe(entry.expected); + } + }); + }); +}); \ No newline at end of file From f01f52d7b3cfd5c8ba359c7bb455a4e2fdadeff7 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 12:12:18 +0100 Subject: [PATCH 032/224] Add see also references --- .../current/packages/support/reflections/hasAllMethods.md | 4 +++- .../archive/current/packages/support/reflections/hasMethod.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/archive/current/packages/support/reflections/hasAllMethods.md b/docs/archive/current/packages/support/reflections/hasAllMethods.md index c8cc4d71..35b1c4aa 100644 --- a/docs/archive/current/packages/support/reflections/hasAllMethods.md +++ b/docs/archive/current/packages/support/reflections/hasAllMethods.md @@ -23,4 +23,6 @@ const a = { hasAllMethods(a, 'foo', 'bar'); // true hasAllMethods(a, 'foo', 'bar', 'zar'); // false -``` \ No newline at end of file +``` + +See also [`hasMethod()`](./hasMethod.md). \ 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 index 5aec48d3..85242af8 100644 --- a/docs/archive/current/packages/support/reflections/hasMethod.md +++ b/docs/archive/current/packages/support/reflections/hasMethod.md @@ -24,4 +24,6 @@ const a = { hasMethod(a, 'foo'); // true hasMethod(a, 'bar'); // true hasMethod(a, 'zar'); // false -``` \ No newline at end of file +``` + +See also [`isMethod()`](./isMethod.md). \ No newline at end of file From 247f660f1e4e1dc8b6744ae919ff800a9a9b0cea Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 12:15:44 +0100 Subject: [PATCH 033/224] Add array target case --- tests/browser/packages/support/reflections/isMethod.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/browser/packages/support/reflections/isMethod.test.js b/tests/browser/packages/support/reflections/isMethod.test.js index 02bd3651..468c4361 100644 --- a/tests/browser/packages/support/reflections/isMethod.test.js +++ b/tests/browser/packages/support/reflections/isMethod.test.js @@ -24,9 +24,11 @@ describe('@aedart/support/reflections', () => { const data = [ { target: undefined, property: undefined, expected: false, name: 'Undefined target' }, { target: null, property: undefined, expected: false, name: 'null target' }, - + { target: instance, property: undefined, expected: false, name: 'undefined property' }, { target: instance, property: null, expected: false, name: 'null property' }, + + { target: [ 'a', 'b', 'c' ], property: 'c', expected: false, name: 'array target' }, { target: instance, property: 'name', expected: false, name: 'field' }, { target: instance, property: 'age', expected: false, name: 'getter' }, From b898aeacaabedffbc3332cab14cf36269294678e Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 12:16:41 +0100 Subject: [PATCH 034/224] Change release notes --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22039529..e503005e 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 * `DEPENDENCIES` symbol and `Identifier` type in `@aedart/contracts/container`. * `dependsOn()`, `dependencies()`, `hasDependencies()`, and `getDependencies()`, in `@aedart/support/container`. * `ClassMethodName` and `ClassMethodReference` type aliases in `@aedart/contracts`. +* `isMethod()` util in `@aedart/support/reflections`. * Add upgrade guide from v0.7.x- to v0.10.x. ### Changed @@ -25,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Root package Typescript dependency changed to `^5.4.2`. * `@typescript-eslint/eslint-plugin` upgraded to `^7.1.1`, in root package. * Decorator return types for `meta()`, `targetMeta()`, and `inheritTargetMeta()` (_continued to cause TS1270 and TS1238 errors_). [#8](https://github.com/aedart/ion/pull/8), [#9](https://github.com/aedart/ion/pull/9). +* Refactored `hasAllMethods()` to use new `isMethod()` internally, in `@aedart/support/reflections`. ### Fixed From 94b96ef03b0345243a9ba3adcc862cbfcc9f577d Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 12:17:11 +0100 Subject: [PATCH 035/224] Refactor, use isMethod() --- packages/support/src/reflections/hasAllMethods.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/support/src/reflections/hasAllMethods.ts b/packages/support/src/reflections/hasAllMethods.ts index 0f960ab8..2a8032d8 100644 --- a/packages/support/src/reflections/hasAllMethods.ts +++ b/packages/support/src/reflections/hasAllMethods.ts @@ -1,4 +1,5 @@ import { isset } from "@aedart/support/misc"; +import { isMethod } from "./isMethod"; /** * Determine if given target object contains all given methods @@ -15,7 +16,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 (!isMethod(target, method)) { return false; } } From 844969341fbf1e0b476e4673d20083ef4b280fc8 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 13:09:07 +0100 Subject: [PATCH 036/224] Add isClassMethodReference() util --- docs/.vuepress/archive/Version0x.ts | 1 + .../reflections/isClassMethodReference.md | 31 +++++++++++ .../src/reflections/isClassMethodReference.ts | 33 +++++++++++ .../isClassMethodReference.test.js | 55 +++++++++++++++++++ 4 files changed, 120 insertions(+) create mode 100644 docs/archive/current/packages/support/reflections/isClassMethodReference.md create mode 100644 packages/support/src/reflections/isClassMethodReference.ts create mode 100644 tests/browser/packages/support/reflections/isClassMethodReference.test.js diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index 8acb80b2..63a6eb36 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -141,6 +141,7 @@ export default PagesCollection.make('v0.x', '/v0x', [ 'packages/support/reflections/hasAllMethods', 'packages/support/reflections/hasMethod', 'packages/support/reflections/hasPrototypeProperty', + 'packages/support/reflections/isClassMethodReference', 'packages/support/reflections/isConstructor', 'packages/support/reflections/isKeySafe', 'packages/support/reflections/isKeyUnsafe', diff --git a/docs/archive/current/packages/support/reflections/isClassMethodReference.md b/docs/archive/current/packages/support/reflections/isClassMethodReference.md new file mode 100644 index 00000000..fe3ef0ff --- /dev/null +++ b/docs/archive/current/packages/support/reflections/isClassMethodReference.md @@ -0,0 +1,31 @@ +--- +title: Is Class Method Reference +description: Determine if value is a class method reference +sidebarDepth: 0 +--- + +# `isClassMethodReference` + +Determine if value is a "_class method reference_". +A class method reference is an `array` with two values: + +- `0 = Constructor | object` Target class constructor or class instance +- `1 = PropertyKey` Name of method (_property key in target_). + +```js +import { isClassMethodReference } from '@aedart/support/reflections'; + +class A { + age = 23; + + foo: () => { /* ...not shown... */ } +} + +const instance = new A(); + +isClassMethodReference([ A, 'age' ]); // false +isClassMethodReference([ instance, 'age' ]); // false + +isClassMethodReference([ A, 'foo' ]); // true +isClassMethodReference([ instance, 'foo' ]); // true +``` \ No newline at end of file diff --git a/packages/support/src/reflections/isClassMethodReference.ts b/packages/support/src/reflections/isClassMethodReference.ts new file mode 100644 index 00000000..c2254c43 --- /dev/null +++ b/packages/support/src/reflections/isClassMethodReference.ts @@ -0,0 +1,33 @@ +import { hasPrototypeProperty } from "./hasPrototypeProperty"; +import { isMethod } from "./isMethod"; + +/** + * Determine if value is a [Class Method Reference]{@link import('@aedart/constracts').ClassMethodReference} + * + * @param {unknown} value + * + * @return {boolean} + */ +export function isClassMethodReference(value: unknown): boolean +{ + if (!Array.isArray(value) || (value as Array).length != 2) { + return false; + } + + const targetType: string = typeof value[0]; + + // If target appears to be a class constructor... + if (targetType == 'function' && hasPrototypeProperty(value[0])) { + // Method must exist in class' prototype + return isMethod(value[0].prototype, value[1]); + } + + // If target is an object (class instance) + if (targetType == 'object') { + // Method must exist in class instance + return isMethod(value[0], value[1]); + } + + // Otherwise target is not valid + return false; +} \ No newline at end of file diff --git a/tests/browser/packages/support/reflections/isClassMethodReference.test.js b/tests/browser/packages/support/reflections/isClassMethodReference.test.js new file mode 100644 index 00000000..5873c8f2 --- /dev/null +++ b/tests/browser/packages/support/reflections/isClassMethodReference.test.js @@ -0,0 +1,55 @@ +import { isClassMethodReference } from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('isClassMethodReference()', () => { + + it('can determine if value is a class method reference', () => { + + const MY_PROP = Symbol('@my_prop') + const MY_METHOD = Symbol('@my_method') + class A { + name = 'Olaf'; + [MY_PROP] = 23; + #title = ''; + + get age() { return 45 } // Checks the actual return type... + set title(v) { this.#title = v } + + foo() {} + [MY_METHOD]() {} + } + + const instance = new A(); + + const data = [ + { value: undefined, expected: false, name: 'Undefined' }, + { value: null, expected: false, name: 'Null' }, + { value: [], expected: false, name: 'Array (empty)' }, + { value: [ A ], expected: false, name: 'Array (with target only)' }, + + { value: [ A, 'does_not_exist' ], expected: false, name: 'Reference to method that does not exist' }, + { value: [ A, 'name' ], expected: false, name: 'Reference to field' }, + { value: [ A, MY_PROP ], expected: false, name: 'Reference to symbol field' }, + { value: [ A, 'age' ], expected: false, name: 'Reference to getter' }, + { value: [ A, 'title' ], expected: false, name: 'Reference to setter' }, + + { value: [ instance, 'does_not_exist' ], expected: false, name: 'Reference via instance to method that does not exist' }, + { value: [ instance, 'name' ], expected: false, name: 'Reference via instance to field' }, + { value: [ instance, MY_PROP ], expected: false, name: 'Reference via instance to symbol field' }, + { value: [ instance, 'age' ], expected: false, name: 'Reference via instance to getter' }, + { value: [ instance, 'title' ], expected: false, name: 'Reference via instance to setter' }, + + { value: [ A, 'foo' ], expected: true, name: 'Reference to method' }, + { value: [ A, MY_METHOD ], expected: true, name: 'Reference to method (symbol)' }, + { value: [ instance, 'foo' ], expected: true, name: 'Reference via instance to method' }, + { value: [ instance, MY_METHOD ], expected: true, name: 'Reference via instance to method (symbol)' }, + ]; + + for (const entry of data) { + expect(isClassMethodReference(entry.value)) + .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) + .toBe(entry.expected); + } + }); + }); +}); \ No newline at end of file From a73d39c798acaf1632293e98bb7bac902c08254c Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 13:09:24 +0100 Subject: [PATCH 037/224] Export isMethod() and isClassMethodReference() --- packages/support/src/reflections/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/support/src/reflections/index.ts b/packages/support/src/reflections/index.ts index 9a784b0b..149ca239 100644 --- a/packages/support/src/reflections/index.ts +++ b/packages/support/src/reflections/index.ts @@ -12,9 +12,11 @@ export * from './hasMethod'; export * from './hasPrototypeProperty'; export * from './isCallable'; export * from './isClassConstructor'; +export * from './isClassMethodReference'; export * from './isConstructor'; export * from './isKeySafe'; export * from './isKeyUnsafe'; +export * from './isMethod'; export * from './isSubclass'; export * from './isSubclassOrLooksLike'; export * from './isWeakKind'; \ No newline at end of file From 0fc29aa1b1516f4860159b031bd38c09d1cd63ec Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 13:12:22 +0100 Subject: [PATCH 038/224] Change call()'s method type to ClassMethodReference A few more types will soon be added, to give call() more flexibility. --- packages/contracts/src/container/Container.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index 16f41b84..d634ff45 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -1,4 +1,7 @@ -import { Constructor } from "@aedart/contracts"; +import { + Constructor, + ClassMethodReference +} from "@aedart/contracts"; import { Alias, Identifier, @@ -176,7 +179,7 @@ export default interface Container build(concrete: Constructor | Binding): T; // TODO: ... - call(method: any, args: any[]): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + call(method: ClassMethodReference, args: any[]): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ /** * Forget binding and resolved instance for given identifier From f0d382a10436393269829546e40677e2c7d5c6d1 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 13:39:01 +0100 Subject: [PATCH 039/224] Deprecate ConstructorOrAbstractConstructor type alias Name was simply way too long / cumbersome to read. A new "ConstructorLike" type alias has been added, which is intended to replace it. --- packages/contracts/src/types.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/contracts/src/types.ts b/packages/contracts/src/types.ts index cf85132b..3da771a1 100644 --- a/packages/contracts/src/types.ts +++ b/packages/contracts/src/types.ts @@ -18,10 +18,20 @@ export type Constructor = new (...args: any[]) => T; export type AbstractConstructor = abstract new (...args: any[]) => T; /** + * @deprecated Since version 0.11 - Use {@link ConstructorLike} instead + * * Constructor or Abstract Constructor type */ export type ConstructorOrAbstractConstructor = Constructor | AbstractConstructor; +/** + * Constructor Like + * + * In this context, a "constructor like" type is either a class constructor, + * or an abstract class constructor. + */ +export type ConstructorLike = Constructor | AbstractConstructor; + /** * Class method name */ From adcbc02b3b0d8e2ed15e176482616205ea603bd0 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 13:56:06 +0100 Subject: [PATCH 040/224] Replace deprecated ConstructorOrAbstractorConstructor type --- .../support/concerns/DescriptorsRepository.ts | 18 +++++++-------- .../src/support/concerns/UsesConcerns.ts | 6 ++--- .../exceptions/AliasConflictException.ts | 6 ++--- .../exceptions/AlreadyRegisteredException.ts | 6 ++--- .../concerns/exceptions/InjectionException.ts | 6 ++--- .../support/src/concerns/ConcernsInjector.ts | 12 +++++----- .../src/concerns/ConfigurationFactory.ts | 6 ++--- packages/support/src/concerns/Repository.ts | 22 +++++++++---------- .../src/concerns/assertIsConcernsOwner.ts | 4 ++-- .../concerns/exceptions/AliasConflictError.ts | 18 +++++++-------- .../exceptions/AlreadyRegisteredError.ts | 14 ++++++------ .../src/concerns/exceptions/InjectionError.ts | 14 ++++++------ .../concerns/exceptions/UnsafeAliasError.ts | 6 ++--- packages/support/src/mixins/Builder.ts | 22 +++++++++---------- packages/support/src/mixins/mix.ts | 6 ++--- .../support/src/reflections/classLooksLike.ts | 4 ++-- .../support/src/reflections/classOwnKeys.ts | 6 ++--- .../src/reflections/getAllParentsOfClass.ts | 12 +++++----- .../reflections/getClassPropertyDescriptor.ts | 6 ++--- .../getClassPropertyDescriptors.ts | 6 ++--- .../src/reflections/getConstructorName.ts | 6 ++--- .../support/src/reflections/getNameOrDesc.ts | 6 ++--- .../src/reflections/getParentOfClass.ts | 10 ++++----- .../support/src/reflections/isSubclass.ts | 6 ++--- .../src/reflections/isSubclassOrLooksLike.ts | 6 ++--- 25 files changed, 117 insertions(+), 117 deletions(-) diff --git a/packages/contracts/src/support/concerns/DescriptorsRepository.ts b/packages/contracts/src/support/concerns/DescriptorsRepository.ts index f3ac87e3..54468904 100644 --- a/packages/contracts/src/support/concerns/DescriptorsRepository.ts +++ b/packages/contracts/src/support/concerns/DescriptorsRepository.ts @@ -1,4 +1,4 @@ -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import ConcernConstructor from './ConcernConstructor'; import UsesConcerns from './UsesConcerns'; @@ -12,14 +12,14 @@ export default interface DescriptorsRepository /** * Returns property descriptors for given target class (recursively) * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {ConstructorLike | 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, + target: ConstructorLike | UsesConcerns | ConcernConstructor, force?: boolean, cache?: boolean ): Record; @@ -27,14 +27,14 @@ export default interface DescriptorsRepository /** * Caches property descriptors for target during the execution of callback. * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {ConstructorLike | 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, + target: ConstructorLike | UsesConcerns | ConcernConstructor, callback: () => any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ forgetAfter?: boolean ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ @@ -42,21 +42,21 @@ export default interface DescriptorsRepository /** * Retrieves the property descriptors for given target and caches them * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {ConstructorLike | 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; + remember(target: ConstructorLike | UsesConcerns | ConcernConstructor, force?: boolean): Record; /** * Deletes cached descriptors for target * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target + * @param {ConstructorLike | UsesConcerns | ConcernConstructor} target * * @return {boolean} */ - forget(target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor): boolean; + forget(target: ConstructorLike | UsesConcerns | ConcernConstructor): boolean; /** * Clears all cached descriptors diff --git a/packages/contracts/src/support/concerns/UsesConcerns.ts b/packages/contracts/src/support/concerns/UsesConcerns.ts index 61a043c5..2548d647 100644 --- a/packages/contracts/src/support/concerns/UsesConcerns.ts +++ b/packages/contracts/src/support/concerns/UsesConcerns.ts @@ -1,4 +1,4 @@ -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import { CONCERN_CLASSES, ALIASES, Alias } from "./index"; import ConcernConstructor from "./ConcernConstructor"; import Owner from "./Owner"; @@ -19,11 +19,11 @@ export default interface UsesConcerns * * @param {...any} [args] * - * @returns {ConstructorOrAbstractConstructor & Owner} + * @returns {ConstructorLike & Owner} */ new( ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ - ): ConstructorOrAbstractConstructor; + ): ConstructorLike; /** * A list of the concern classes to be used by this target class. diff --git a/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts b/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts index e9214934..2598d26f 100644 --- a/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts +++ b/packages/contracts/src/support/concerns/exceptions/AliasConflictException.ts @@ -1,4 +1,4 @@ -import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { ConstructorLike } from "@aedart/contracts"; import InjectionException from "./InjectionException"; import UsesConcerns from "../UsesConcerns"; import { Alias } from '../types' @@ -34,7 +34,7 @@ export default interface AliasConflictException extends InjectionException * * @readonly * - * @type {ConstructorOrAbstractConstructor | UsesConcerns} + * @type {ConstructorLike | UsesConcerns} */ - readonly source: ConstructorOrAbstractConstructor | UsesConcerns; + readonly source: ConstructorLike | UsesConcerns; } \ No newline at end of file diff --git a/packages/contracts/src/support/concerns/exceptions/AlreadyRegisteredException.ts b/packages/contracts/src/support/concerns/exceptions/AlreadyRegisteredException.ts index a514b947..64c0cba2 100644 --- a/packages/contracts/src/support/concerns/exceptions/AlreadyRegisteredException.ts +++ b/packages/contracts/src/support/concerns/exceptions/AlreadyRegisteredException.ts @@ -1,5 +1,5 @@ import { InjectionException } from "@aedart/contracts/support/concerns"; -import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { ConstructorLike } from "@aedart/contracts"; import UsesConcerns from "../UsesConcerns"; /** @@ -16,7 +16,7 @@ export default interface AlreadyRegisteredException extends InjectionException * * @readonly * - * @type {ConstructorOrAbstractConstructor|UsesConcerns} + * @type {ConstructorLike | UsesConcerns} */ - readonly source: ConstructorOrAbstractConstructor | UsesConcerns; + readonly source: ConstructorLike | 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 a2bac41b..a4991590 100644 --- a/packages/contracts/src/support/concerns/exceptions/InjectionException.ts +++ b/packages/contracts/src/support/concerns/exceptions/InjectionException.ts @@ -1,4 +1,4 @@ -import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { ConstructorLike } from "@aedart/contracts"; import ConcernException from "./ConcernException"; import UsesConcerns from "../UsesConcerns"; @@ -14,7 +14,7 @@ export default interface InjectionException extends ConcernException * * @readonly * - * @type {ConstructorOrAbstractConstructor|UsesConcerns} + * @type {ConstructorLike | UsesConcerns} */ - readonly target: ConstructorOrAbstractConstructor | UsesConcerns; + readonly target: ConstructorLike | UsesConcerns; } \ No newline at end of file diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 6d72ca7b..3d093fe1 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -13,7 +13,7 @@ import type { AliasDescriptorFactory, RegistrationAware } from "@aedart/contracts/support/concerns"; -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import { CONCERN_CLASSES, ALIASES, @@ -388,7 +388,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, true); - throw new AlreadyRegisteredError(target as ConstructorOrAbstractConstructor, concern, source as ConstructorOrAbstractConstructor); + throw new AlreadyRegisteredError(target as ConstructorLike, concern, source as ConstructorLike); } registry.push(concern); @@ -439,8 +439,8 @@ export default class ConcernsInjector implements Injector const wasDefined: boolean = Reflect.defineProperty((target as object), property, descriptor); if (!wasDefined) { - const reason: string = failMessage || `Unable to define "${property.toString()}" property in target ${getNameOrDesc(target as ConstructorOrAbstractConstructor)}`; - throw new InjectionError(target as ConstructorOrAbstractConstructor, null, reason); + const reason: string = failMessage || `Unable to define "${property.toString()}" property in target ${getNameOrDesc(target as ConstructorLike)}`; + throw new InjectionError(target as ConstructorLike, null, reason); } return target; @@ -460,7 +460,7 @@ export default class ConcernsInjector implements Injector */ protected findSourceOf(concern: ConcernConstructor, target: object, includeTarget: boolean = false): object | null { - const parents = getAllParentsOfClass(target as ConstructorOrAbstractConstructor, includeTarget).reverse(); + const parents = getAllParentsOfClass(target as ConstructorLike, includeTarget).reverse(); for (const parent of parents) { if (Reflect.has(parent, CONCERN_CLASSES) && (parent[CONCERN_CLASSES as keyof typeof parent] as ConcernConstructor[]).includes(concern)) { @@ -485,7 +485,7 @@ export default class ConcernsInjector implements Injector { const output: Map = new Map(); - const parents = getAllParentsOfClass(target as ConstructorOrAbstractConstructor, includeTarget).reverse(); + const parents = getAllParentsOfClass(target as ConstructorLike, includeTarget).reverse(); for (const parent of parents) { if (!Reflect.has(parent, ALIASES)) { continue; diff --git a/packages/support/src/concerns/ConfigurationFactory.ts b/packages/support/src/concerns/ConfigurationFactory.ts index 0d8746b0..193afd87 100644 --- a/packages/support/src/concerns/ConfigurationFactory.ts +++ b/packages/support/src/concerns/ConfigurationFactory.ts @@ -1,4 +1,4 @@ -import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { ConstructorLike } from "@aedart/contracts"; import type { Aliases, ConcernConstructor, @@ -74,8 +74,8 @@ export default class ConfigurationFactory implements Factory } // 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 } }); + const reason: string = `${getNameOrDesc(entry as ConstructorLike)} must be a valid Concern class or Concern Configuration` + throw new InjectionError(target as ConstructorLike, null, reason, { cause: { entry: entry } }); } /** diff --git a/packages/support/src/concerns/Repository.ts b/packages/support/src/concerns/Repository.ts index 4a4626c8..412e93e3 100644 --- a/packages/support/src/concerns/Repository.ts +++ b/packages/support/src/concerns/Repository.ts @@ -1,4 +1,4 @@ -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import type { ConcernConstructor, UsesConcerns, DescriptorsRepository } from "@aedart/contracts/support/concerns"; import { getClassPropertyDescriptors } from "@aedart/support/reflections"; @@ -12,12 +12,12 @@ export default class Repository implements DescriptorsRepository /** * In-memory cache property descriptors for target class and concern classes * - * @type {WeakMap>} + * @type {WeakMap>} * * @private */ #store: WeakMap< - ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, + ConstructorLike | UsesConcerns | ConcernConstructor, Record >; @@ -31,14 +31,14 @@ export default class Repository implements DescriptorsRepository /** * Returns property descriptors for given target class (recursively) * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {ConstructorLike | 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, + target: ConstructorLike | UsesConcerns | ConcernConstructor, force: boolean = false, cache: boolean = false ): Record @@ -58,12 +58,12 @@ export default class Repository implements DescriptorsRepository /** * Caches property descriptors for target during the execution of callback. * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {ConstructorLike | 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, + target: ConstructorLike | UsesConcerns | ConcernConstructor, callback: () => any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ forgetAfter: boolean = true ): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ @@ -82,12 +82,12 @@ export default class Repository implements DescriptorsRepository /** * Retrieves the property descriptors for given target and caches them * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {ConstructorLike | 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 + public remember(target: ConstructorLike | UsesConcerns | ConcernConstructor, force: boolean = false): Record { return this.get(target, force, true); } @@ -95,11 +95,11 @@ export default class Repository implements DescriptorsRepository /** * Deletes cached descriptors for target * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target + * @param {ConstructorLike | UsesConcerns | ConcernConstructor} target * * @return {boolean} */ - public forget(target: ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor): boolean + public forget(target: ConstructorLike | UsesConcerns | ConcernConstructor): boolean { return this.#store.delete(target); } diff --git a/packages/support/src/concerns/assertIsConcernsOwner.ts b/packages/support/src/concerns/assertIsConcernsOwner.ts index 73aff60d..09c3ea5d 100644 --- a/packages/support/src/concerns/assertIsConcernsOwner.ts +++ b/packages/support/src/concerns/assertIsConcernsOwner.ts @@ -1,4 +1,4 @@ -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import { getNameOrDesc } from "@aedart/support/reflections"; import { isConcernsOwner } from "./isConcernsOwner"; @@ -14,7 +14,7 @@ import { isConcernsOwner } from "./isConcernsOwner"; export function assertIsConcernsOwner(instance: object): void { if (!isConcernsOwner(instance)) { - const msg: string = `${getNameOrDesc(instance as ConstructorOrAbstractConstructor)} is not a concerns owner`; + const msg: string = `${getNameOrDesc(instance as ConstructorLike)} 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/exceptions/AliasConflictError.ts b/packages/support/src/concerns/exceptions/AliasConflictError.ts index 0d94f757..33a2f4b8 100644 --- a/packages/support/src/concerns/exceptions/AliasConflictError.ts +++ b/packages/support/src/concerns/exceptions/AliasConflictError.ts @@ -1,4 +1,4 @@ -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import type { AliasConflictException, ConcernConstructor, UsesConcerns, Alias } from "@aedart/contracts/support/concerns"; import InjectionError from "./InjectionError"; import { getNameOrDesc } from "@aedart/support/reflections"; @@ -38,26 +38,26 @@ export default class AliasConflictError extends InjectionError implements AliasC * @readonly * @private * - * @type {ConstructorOrAbstractConstructor | UsesConcerns} + * @type {ConstructorLike | UsesConcerns} */ - readonly #source: ConstructorOrAbstractConstructor | UsesConcerns; + readonly #source: ConstructorLike | UsesConcerns; /** * Create a new Alias Conflict Error instance * - * @param {ConstructorOrAbstractConstructor | UsesConcerns} target + * @param {ConstructorLike | UsesConcerns} target * @param {ConcernConstructor} concern * @param {Alias} alias * @param {PropertyKey} key - * @param {ConstructorOrAbstractConstructor | UsesConcerns} source + * @param {ConstructorLike | UsesConcerns} source * @param {ErrorOptions} [options] */ constructor( - target: ConstructorOrAbstractConstructor | UsesConcerns, + target: ConstructorLike | UsesConcerns, concern: ConcernConstructor, alias: Alias, key: PropertyKey, - source: ConstructorOrAbstractConstructor | UsesConcerns, + source: ConstructorLike | UsesConcerns, options?: ErrorOptions ) { const reason: string = (target === source) @@ -106,9 +106,9 @@ export default class AliasConflictError extends InjectionError implements AliasC * * @readonly * - * @type {ConstructorOrAbstractConstructor | UsesConcerns} + * @type {ConstructorLike | UsesConcerns} */ - get source(): ConstructorOrAbstractConstructor | UsesConcerns + get source(): ConstructorLike | UsesConcerns { return this.#source; } diff --git a/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts b/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts index 28d465e8..599c2026 100644 --- a/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts +++ b/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts @@ -3,7 +3,7 @@ import type { ConcernConstructor, UsesConcerns } from "@aedart/contracts/support/concerns"; -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import { configureCustomError } from "@aedart/support/exceptions"; import { getNameOrDesc } from "@aedart/support/reflections"; import InjectionError from "./InjectionError"; @@ -22,14 +22,14 @@ export default class AlreadyRegisteredError extends InjectionError implements Al * @readonly * @private * - * @type {ConstructorOrAbstractConstructor|UsesConcerns} + * @type {ConstructorLike|UsesConcerns} */ - readonly #source: ConstructorOrAbstractConstructor | UsesConcerns; + readonly #source: ConstructorLike | UsesConcerns; constructor( - target: ConstructorOrAbstractConstructor | UsesConcerns, + target: ConstructorLike | UsesConcerns, concern: ConcernConstructor, - source: ConstructorOrAbstractConstructor | UsesConcerns, + source: ConstructorLike | UsesConcerns, message?: string, options?: ErrorOptions ) { @@ -53,9 +53,9 @@ export default class AlreadyRegisteredError extends InjectionError implements Al * * @readonly * - * @returns {ConstructorOrAbstractConstructor | UsesConcerns} + * @returns {ConstructorLike | UsesConcerns} */ - get source(): ConstructorOrAbstractConstructor | UsesConcerns + get source(): ConstructorLike | UsesConcerns { return this.#source; } diff --git a/packages/support/src/concerns/exceptions/InjectionError.ts b/packages/support/src/concerns/exceptions/InjectionError.ts index e5c31436..3f83ffbb 100644 --- a/packages/support/src/concerns/exceptions/InjectionError.ts +++ b/packages/support/src/concerns/exceptions/InjectionError.ts @@ -1,5 +1,5 @@ import type { ConcernConstructor, InjectionException, UsesConcerns } from "@aedart/contracts/support/concerns"; -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } 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|UsesConcerns} + * @type {ConstructorLike|UsesConcerns} */ - readonly #target: ConstructorOrAbstractConstructor | UsesConcerns; + readonly #target: ConstructorLike | UsesConcerns; /** * Create a new Injection Error instance * - * @param {ConstructorOrAbstractConstructor | UsesConcerns} target + * @param {ConstructorLike | UsesConcerns} target * @param {ConcernConstructor | null} concern * @param {string} message * @param {ErrorOptions} [options] */ constructor( - target: ConstructorOrAbstractConstructor | UsesConcerns, + target: ConstructorLike | UsesConcerns, concern: ConcernConstructor | null, message: string, options?: ErrorOptions @@ -48,9 +48,9 @@ export default class InjectionError extends ConcernError implements InjectionExc * * @readonly * - * @returns {ConstructorOrAbstractConstructor | UsesConcerns} + * @returns {ConstructorLike | UsesConcerns} */ - get target(): ConstructorOrAbstractConstructor | UsesConcerns + get target(): ConstructorLike | UsesConcerns { return this.#target; } diff --git a/packages/support/src/concerns/exceptions/UnsafeAliasError.ts b/packages/support/src/concerns/exceptions/UnsafeAliasError.ts index 5d30e3bd..9ed520a9 100644 --- a/packages/support/src/concerns/exceptions/UnsafeAliasError.ts +++ b/packages/support/src/concerns/exceptions/UnsafeAliasError.ts @@ -2,7 +2,7 @@ import type { ConcernConstructor, UsesConcerns, UnsafeAliasException } from "@aedart/contracts/support/concerns"; -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import { configureCustomError } from "@aedart/support/exceptions"; import InjectionError from "./InjectionError"; import {getNameOrDesc} from "@aedart/support/reflections"; @@ -33,7 +33,7 @@ export default class UnsafeAliasError extends InjectionError implements UnsafeAl /** * Create a new Unsafe Alias Error instance * - * @param {ConstructorOrAbstractConstructor | UsesConcerns} target + * @param {ConstructorLike | 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 | UsesConcerns, + target: ConstructorLike | UsesConcerns, concern: ConcernConstructor, alias: PropertyKey, key: PropertyKey, diff --git a/packages/support/src/mixins/Builder.ts b/packages/support/src/mixins/Builder.ts index 744fc76d..f2a1d658 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 { ConstructorLike } from "@aedart/contracts"; import type { MixinFunction } from "@aedart/contracts/support/mixins"; /** @@ -18,17 +18,17 @@ export default class Builder /** * The target superclass * - * @type {ConstructorOrAbstractConstructor} + * @type {ConstructorLike} * @private */ - readonly #superclass: ConstructorOrAbstractConstructor; + readonly #superclass: ConstructorLike; /** * Create a new Mixin Builder instance * - * @param {ConstructorOrAbstractConstructor} [superclass=class {}] + * @param {ConstructorLike} [superclass=class {}] */ - constructor(superclass:ConstructorOrAbstractConstructor = class {} as ConstructorOrAbstractConstructor) { + constructor(superclass:ConstructorLike = class {} as ConstructorLike) { this.#superclass = superclass; } @@ -37,12 +37,12 @@ export default class Builder * * @param {...MixinFunction} mixins * - * @return {ConstructorOrAbstractConstructor} Subclass of given superclass with given mixins applied + * @return {ConstructorLike} Subclass of given superclass with given mixins applied */ - public with(...mixins: MixinFunction[]): ConstructorOrAbstractConstructor + public with(...mixins: MixinFunction[]): ConstructorLike { return mixins.reduce(( - superclass: ConstructorOrAbstractConstructor, + superclass: ConstructorLike, 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 as ConstructorOrAbstractConstructor) as ConstructorOrAbstractConstructor; + }, this.#superclass as ConstructorLike) as ConstructorLike; } /** @@ -63,11 +63,11 @@ export default class Builder * * @param {...MixinFunction} [mixins] Ignored * - * @return {ConstructorOrAbstractConstructor} The superclass + * @return {ConstructorLike} The superclass */ public none( ...mixins: MixinFunction[] /* eslint-disable-line @typescript-eslint/no-unused-vars */ - ): ConstructorOrAbstractConstructor + ): ConstructorLike { return this.#superclass; } diff --git a/packages/support/src/mixins/mix.ts b/packages/support/src/mixins/mix.ts index dbe6dc23..9c6235e7 100644 --- a/packages/support/src/mixins/mix.ts +++ b/packages/support/src/mixins/mix.ts @@ -1,4 +1,4 @@ -import type {ConstructorOrAbstractConstructor} from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import Builder from "./Builder"; /** @@ -23,11 +23,11 @@ import Builder from "./Builder"; * * @template T = object * - * @param {ConstructorOrAbstractConstructor} [superclass=class {}] + * @param {ConstructorLike} [superclass=class {}] * * @return {Builder} New Mixin Builder instance */ -export function mix(superclass:ConstructorOrAbstractConstructor = class {}): Builder +export function mix(superclass:ConstructorLike = class {} as ConstructorLike): Builder { // The following source code is an adaptation of Justin Fagnani's "mixwith.js" (Apache License 2.0) // @see https://github.com/justinfagnani/mixwith.js diff --git a/packages/support/src/reflections/classLooksLike.ts b/packages/support/src/reflections/classLooksLike.ts index e37c9df1..93652252 100644 --- a/packages/support/src/reflections/classLooksLike.ts +++ b/packages/support/src/reflections/classLooksLike.ts @@ -1,5 +1,5 @@ import type { ClassBlueprint } from "@aedart/contracts/support/reflections"; -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import { includesAll } from "@aedart/support/arrays"; import { hasPrototypeProperty } from "./hasPrototypeProperty"; import { classOwnKeys } from "./classOwnKeys"; @@ -48,7 +48,7 @@ export function classLooksLike(target: object, blueprint: ClassBlueprint): boole // 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), + classOwnKeys(target as ConstructorLike, true), (blueprint.members as PropertyKey[]) ); } diff --git a/packages/support/src/reflections/classOwnKeys.ts b/packages/support/src/reflections/classOwnKeys.ts index e117df3d..0ff3bf3a 100644 --- a/packages/support/src/reflections/classOwnKeys.ts +++ b/packages/support/src/reflections/classOwnKeys.ts @@ -1,11 +1,11 @@ -import type {ConstructorOrAbstractConstructor} from "@aedart/contracts"; +import type { ConstructorLike } 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 {ConstructorLike} target * @param {boolean} [recursive=false] If `true`, then target's parent prototypes are traversed and all * property keys are returned. * @@ -13,7 +13,7 @@ import { getAllParentsOfClass } from "@aedart/support/reflections/getAllParentsO * * @throws {TypeError} If target object does not have "prototype" property */ -export function classOwnKeys(target: ConstructorOrAbstractConstructor, recursive: boolean = false): PropertyKey[] +export function classOwnKeys(target: ConstructorLike, recursive: boolean = false): PropertyKey[] { assertHasPrototypeProperty(target); diff --git a/packages/support/src/reflections/getAllParentsOfClass.ts b/packages/support/src/reflections/getAllParentsOfClass.ts index bb79372b..462c3656 100644 --- a/packages/support/src/reflections/getAllParentsOfClass.ts +++ b/packages/support/src/reflections/getAllParentsOfClass.ts @@ -1,4 +1,4 @@ -import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { ConstructorLike } from "@aedart/contracts"; import { getParentOfClass } from "./getParentOfClass"; import { isset } from "@aedart/support/misc"; @@ -7,25 +7,25 @@ import { isset } from "@aedart/support/misc"; * * @see {getParentOfClass} * - * @param {ConstructorOrAbstractConstructor} target The target class. + * @param {ConstructorLike} target The target class. * @param {boolean} [includeTarget=false] If `true`, then given target is included in the output as the first element. * - * @returns {ConstructorOrAbstractConstructor[]} List of parent classes, ordered by the top-most parent class first. + * @returns {ConstructorLike[]} List of parent classes, ordered by the top-most parent class first. * * @throws {TypeError} */ -export function getAllParentsOfClass(target: ConstructorOrAbstractConstructor, includeTarget: boolean = false): ConstructorOrAbstractConstructor[] +export function getAllParentsOfClass(target: ConstructorLike, includeTarget: boolean = false): ConstructorLike[] { if (!isset(target)) { throw new TypeError('getAllParentsOfClass() expects a target class as argument, undefined given'); } - const output: ConstructorOrAbstractConstructor[] = []; + const output: ConstructorLike[] = []; if (includeTarget) { output.push(target); } - let parent: ConstructorOrAbstractConstructor | null = getParentOfClass(target); + let parent: ConstructorLike | null = getParentOfClass(target); while (parent !== null) { output.push(parent); diff --git a/packages/support/src/reflections/getClassPropertyDescriptor.ts b/packages/support/src/reflections/getClassPropertyDescriptor.ts index 839a1a3d..5c5c01b4 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptor.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptor.ts @@ -1,4 +1,4 @@ -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import { assertHasPrototypeProperty } from "./assertHasPrototypeProperty"; /** @@ -6,7 +6,7 @@ import { assertHasPrototypeProperty } from "./assertHasPrototypeProperty"; * * @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 {ConstructorLike} target Class that contains property in its prototype * @param {PropertyKey} key Name of the property * * @return {PropertyDescriptor|undefined} Property descriptor or `undefined` if property does @@ -14,7 +14,7 @@ import { assertHasPrototypeProperty } from "./assertHasPrototypeProperty"; * * @throws {TypeError} If target is not an object or has no prototype */ -export function getClassPropertyDescriptor(target: ConstructorOrAbstractConstructor, key: PropertyKey): PropertyDescriptor|undefined +export function getClassPropertyDescriptor(target: ConstructorLike, key: PropertyKey): PropertyDescriptor|undefined { assertHasPrototypeProperty(target); diff --git a/packages/support/src/reflections/getClassPropertyDescriptors.ts b/packages/support/src/reflections/getClassPropertyDescriptors.ts index 7a19f3d5..6cd5c7f2 100644 --- a/packages/support/src/reflections/getClassPropertyDescriptors.ts +++ b/packages/support/src/reflections/getClassPropertyDescriptors.ts @@ -1,4 +1,4 @@ -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import { getClassPropertyDescriptor } from "./getClassPropertyDescriptor"; import { assertHasPrototypeProperty } from "./assertHasPrototypeProperty"; import { getAllParentsOfClass } from "./getAllParentsOfClass"; @@ -9,7 +9,7 @@ import { merge } from "@aedart/support/objects"; * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getOwnPropertyDescriptor * - * @param {ConstructorOrAbstractConstructor} target The target class + * @param {ConstructorLike} target The target class * @param {boolean} [recursive=false] If `true`, then target's parent prototypes are traversed. * Descriptors are merged, such that the top-most class' descriptors * are returned. @@ -19,7 +19,7 @@ import { merge } from "@aedart/support/objects"; * * @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: ConstructorLike, recursive: boolean = false): Record { assertHasPrototypeProperty(target); diff --git a/packages/support/src/reflections/getConstructorName.ts b/packages/support/src/reflections/getConstructorName.ts index d9b20ee6..128b7523 100644 --- a/packages/support/src/reflections/getConstructorName.ts +++ b/packages/support/src/reflections/getConstructorName.ts @@ -1,15 +1,15 @@ -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import { isset } from "@aedart/support/objects"; /** * Returns target class' constructor name, if available * - * @param {ConstructorOrAbstractConstructor} target + * @param {ConstructorLike} target * @param {string|null} [defaultValue=null] A default string value to return if target has no constructor name * * @return {string|null} Constructor name, or default value */ -export function getConstructorName(target: ConstructorOrAbstractConstructor, defaultValue: string|null = null): string|null +export function getConstructorName(target: ConstructorLike, defaultValue: string|null = null): string|null { if (!isset(target, [ 'prototype', 'constructor', 'name' ])) { return defaultValue; diff --git a/packages/support/src/reflections/getNameOrDesc.ts b/packages/support/src/reflections/getNameOrDesc.ts index a6a865ce..6bb25d45 100644 --- a/packages/support/src/reflections/getNameOrDesc.ts +++ b/packages/support/src/reflections/getNameOrDesc.ts @@ -1,4 +1,4 @@ -import type {ConstructorOrAbstractConstructor} from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import { descTag } from "@aedart/support/misc"; import { getConstructorName } from "./getConstructorName"; @@ -13,11 +13,11 @@ import { getConstructorName } from "./getConstructorName"; * @see getConstructorName * @see descTag * - * @param {ConstructorOrAbstractConstructor} target + * @param {ConstructorLike} target * * @return {string} */ -export function getNameOrDesc(target: ConstructorOrAbstractConstructor): string +export function getNameOrDesc(target: ConstructorLike): string { return getConstructorName(target, descTag(target)) as string; } \ No newline at end of file diff --git a/packages/support/src/reflections/getParentOfClass.ts b/packages/support/src/reflections/getParentOfClass.ts index c84acffb..2538a92b 100644 --- a/packages/support/src/reflections/getParentOfClass.ts +++ b/packages/support/src/reflections/getParentOfClass.ts @@ -1,4 +1,4 @@ -import { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import { ConstructorLike } from "@aedart/contracts"; import { FUNCTION_PROTOTYPE } from "@aedart/contracts/support/reflections"; import {isset} from "@aedart/support/misc"; @@ -8,13 +8,13 @@ import {isset} from "@aedart/support/misc"; * **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 + * @param {ConstructorLike} target The target class * - * @returns {ConstructorOrAbstractConstructor | null} Parent class or `null`, if target has no parent class. + * @returns {ConstructorLike | null} Parent class or `null`, if target has no parent class. * * @throws {TypeError} */ -export function getParentOfClass(target: ConstructorOrAbstractConstructor): ConstructorOrAbstractConstructor | null +export function getParentOfClass(target: ConstructorLike): ConstructorLike | null { if (!isset(target)) { throw new TypeError('getParentOfClass() expects a target class as argument, undefined given'); @@ -25,5 +25,5 @@ export function getParentOfClass(target: ConstructorOrAbstractConstructor): Cons return null; } - return parent as ConstructorOrAbstractConstructor; + return parent as ConstructorLike; } \ No newline at end of file diff --git a/packages/support/src/reflections/isSubclass.ts b/packages/support/src/reflections/isSubclass.ts index b8e60d88..dd92faf3 100644 --- a/packages/support/src/reflections/isSubclass.ts +++ b/packages/support/src/reflections/isSubclass.ts @@ -1,4 +1,4 @@ -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import { hasPrototypeProperty } from "./hasPrototypeProperty"; /** @@ -9,11 +9,11 @@ import { hasPrototypeProperty } from "./hasPrototypeProperty"; * However, if given target or superclass does not have a prototype property, then `false` is returned._ * * @param {object} target - * @param {ConstructorOrAbstractConstructor} superclass + * @param {ConstructorLike} superclass * * @returns {boolean} `true` if target is a subclass of given superclass, `false` otherwise. */ -export function isSubclass(target: object, superclass: ConstructorOrAbstractConstructor): boolean +export function isSubclass(target: object, superclass: ConstructorLike): boolean { if (!hasPrototypeProperty(target) || !hasPrototypeProperty(superclass) || target === superclass) { return false; diff --git a/packages/support/src/reflections/isSubclassOrLooksLike.ts b/packages/support/src/reflections/isSubclassOrLooksLike.ts index ef8d7e05..c9b8cefd 100644 --- a/packages/support/src/reflections/isSubclassOrLooksLike.ts +++ b/packages/support/src/reflections/isSubclassOrLooksLike.ts @@ -1,4 +1,4 @@ -import type { ConstructorOrAbstractConstructor } from "@aedart/contracts"; +import type { ConstructorLike } from "@aedart/contracts"; import type { ClassBlueprint } from "@aedart/contracts/support/reflections"; import { isSubclass } from "./isSubclass"; import { classLooksLike } from "./classLooksLike"; @@ -12,14 +12,14 @@ import { classLooksLike } from "./classLooksLike"; * @see classLooksLike * * @param {object} target - * @param {ConstructorOrAbstractConstructor} superclass + * @param {ConstructorLike} superclass * @param {ClassBlueprint} blueprint * * @throws {TypeError} */ export function isSubclassOrLooksLike( target: object, - superclass: ConstructorOrAbstractConstructor, + superclass: ConstructorLike, blueprint: ClassBlueprint ): boolean { From 1cc3ae06d6f2d0a29c3fce9553100b758dda9ebd Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 13:59:00 +0100 Subject: [PATCH 041/224] Change release notes --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e503005e..6639e55b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,13 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `dependsOn()`, `dependencies()`, `hasDependencies()`, and `getDependencies()`, in `@aedart/support/container`. * `ClassMethodName` and `ClassMethodReference` type aliases in `@aedart/contracts`. * `isMethod()` util in `@aedart/support/reflections`. +* `ConstructorLike` type alias, in `@aedart/constracts`. * Add upgrade guide from v0.7.x- to v0.10.x. ### Changed **Breaking** -* Added `hasAny()` method in `TargetRepository` interface, in `@aedart/contracts/meta` +* Added `hasAny()` method in `TargetRepository` interface, in `@aedart/contracts/meta`. **Non-breaking Changes** @@ -27,12 +28,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `@typescript-eslint/eslint-plugin` upgraded to `^7.1.1`, in root package. * Decorator return types for `meta()`, `targetMeta()`, and `inheritTargetMeta()` (_continued to cause TS1270 and TS1238 errors_). [#8](https://github.com/aedart/ion/pull/8), [#9](https://github.com/aedart/ion/pull/9). * Refactored `hasAllMethods()` to use new `isMethod()` internally, in `@aedart/support/reflections`. +* Refactored all components that used deprecated `ConstructorOrAbstractConstructor` to use new `ConstructorLike` type alias. ### Fixed * Decorator types aliases (_TS1270 and TS1238 issues when applying the various decorator and decorator result types_). [#8](https://github.com/aedart/ion/pull/8). * Broken link in docs for `isArrayLike`. +### Deprecated + +* `ConstructorOrAbstractConstructor` type alias. It has been replaced with the new `ConstructorLike` type., in `@aedart/constracts`. + ## [0.10.0] - 2024-03-07 ### Added From 72d2c3625fbcf5c90e6b5bce6cc382b403cf57fc Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 15:11:08 +0100 Subject: [PATCH 042/224] Mark isClassConstructor() as stable --- .../support/src/reflections/isClassConstructor.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/support/src/reflections/isClassConstructor.ts b/packages/support/src/reflections/isClassConstructor.ts index f3a1dae1..e3d535b1 100644 --- a/packages/support/src/reflections/isClassConstructor.ts +++ b/packages/support/src/reflections/isClassConstructor.ts @@ -1,30 +1,26 @@ /** - * **WARNING**: _Method is currently unsafe to use! It is subject for breaking changes or possible removal._ - * - * Determine if given argument is a class constructor + * Determine if given value is a class constructor (es6) * * @see https://github.com/caitp/TC39-Proposals/blob/trunk/tc39-reflect-isconstructor-iscallable.md * - * @param {unknown} argument + * @param {unknown} value * * @return {boolean} */ -export function isClassConstructor(argument: unknown): boolean +export function isClassConstructor(value: unknown): boolean { - // TODO: WARNING: Method is currently unsafe to use! It is subject for breaking changes or possible removal. - // Source is heavily inspired by Denis Pushkarev's Core-js implementation of // Function.isCallable / Function.isConstructor, License MIT // @see https://github.com/zloirock/core-js#function-iscallable-isconstructor- - if (typeof argument != 'function') { + if (typeof value != 'function') { return false; } try { // Obtain a small part of the argument's string representation, to avoid // too large string from being processed by regex. - const source: string = argument.toString().slice(0, 25); + const source: string = value.toString().slice(0, 25); // Determine if source starts with "class". return new RegExp(/^\s*class\b/).test(source); From 60a163cbffeb83fbe3bb1ca434c530dc04a5b7b3 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 15:11:30 +0100 Subject: [PATCH 043/224] Extract tests into own file for isClassConstructor() --- .../isCallable-isConstructor.test.js | 27 --------------- .../reflections/isClassConstructor.test.js | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 27 deletions(-) create mode 100644 tests/browser/packages/support/reflections/isClassConstructor.test.js diff --git a/tests/browser/packages/support/reflections/isCallable-isConstructor.test.js b/tests/browser/packages/support/reflections/isCallable-isConstructor.test.js index 6006a5bb..1c18e797 100644 --- a/tests/browser/packages/support/reflections/isCallable-isConstructor.test.js +++ b/tests/browser/packages/support/reflections/isCallable-isConstructor.test.js @@ -73,31 +73,4 @@ describe('@aedart/support/reflections', () => { }); }); - - describe('isClassConstructor', () => { - - // TODO: Unsafe / Unstable... - - it('can determine if is class constructor', () => { - - const data = [ - { value: null, expected: false }, - { value: {}, expected: false }, - { value: [], expected: false }, - { value: function() {}, expected: false }, - { value: () => {}, expected: false }, - { value: Array, expected: false }, - { value: class {}, expected: true }, - ]; - - data.forEach((entry, index) => { - - let result = isClassConstructor(entry.value); - expect(result) - .withContext(`Value at index ${index} was expected to be ${entry.expected}`) - .toBe(entry.expected); - }); - }); - - }); }); \ No newline at end of file diff --git a/tests/browser/packages/support/reflections/isClassConstructor.test.js b/tests/browser/packages/support/reflections/isClassConstructor.test.js new file mode 100644 index 00000000..e4178fcb --- /dev/null +++ b/tests/browser/packages/support/reflections/isClassConstructor.test.js @@ -0,0 +1,34 @@ +import { + isClassConstructor, +} from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('isClassConstructor', () => { + + it('can determine if is class constructor', () => { + + class A {} + + const data = [ + { value: undefined, expected: false, name: 'undefined' }, + { value: null, expected: false, name: 'null' }, + { value: {}, expected: false, name: 'object' }, + { value: [], expected: false, name: 'array' }, + { value: Array, expected: false, name: 'array (object)' }, + { value: function() {}, expected: false, name: 'function' }, + { value: () => {}, expected: false, name: 'arrow function' }, + + { value: A, expected: true, name: 'class' }, + { value: class {}, expected: true, name: 'class (anonymous)' }, + ]; + + data.forEach((entry, index) => { + + let result = isClassConstructor(entry.value); + expect(result) + .withContext(`${entry.name} was expected to be ${entry.expected}`) + .toBe(entry.expected); + }); + }); + }); +}); \ No newline at end of file From b42507827580f92c909e6a6055f3a4058c49e8da Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 15:18:26 +0100 Subject: [PATCH 044/224] Add docs for isClassConstructor --- docs/.vuepress/archive/Version0x.ts | 1 + .../support/reflections/isClassConstructor.md | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 docs/archive/current/packages/support/reflections/isClassConstructor.md diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index 63a6eb36..deb8fba1 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -141,6 +141,7 @@ export default PagesCollection.make('v0.x', '/v0x', [ 'packages/support/reflections/hasAllMethods', 'packages/support/reflections/hasMethod', 'packages/support/reflections/hasPrototypeProperty', + 'packages/support/reflections/isClassConstructor', 'packages/support/reflections/isClassMethodReference', 'packages/support/reflections/isConstructor', 'packages/support/reflections/isKeySafe', diff --git a/docs/archive/current/packages/support/reflections/isClassConstructor.md b/docs/archive/current/packages/support/reflections/isClassConstructor.md new file mode 100644 index 00000000..f156d31e --- /dev/null +++ b/docs/archive/current/packages/support/reflections/isClassConstructor.md @@ -0,0 +1,33 @@ +--- +title: Is Class Constructor +description: Determine if value is a class constructor. +sidebarDepth: 0 +--- + +# `isClassConstructor` + +The `isClassConstructor()` is able to determine if a value is a class constructor. + +::: warning Caution +`isClassConstructor()` will only be able to return `true` for classes that are defined using the `class` keyword. See [ES6 classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) for additional information. +::: + + +```js +import { isClassConstructor } from "@aedart/support/reflections"; + +isClassConstructor(null); // false +isClassConstructor({}); // false +isClassConstructor([]); // false +isClassConstructor(function() {}); // true +isClassConstructor(() => {}); // false +isClassConstructor(Array); // false + +isClassConstructor(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_). + +See also [`isConstructor()`](./isConstructor.md). From d519d25ef5c4df4c2c8cebcb247cb3b84db093b8 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 15:18:55 +0100 Subject: [PATCH 045/224] Add see also reference to isClassConstructor() --- .../current/packages/support/reflections/isConstructor.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/archive/current/packages/support/reflections/isConstructor.md b/docs/archive/current/packages/support/reflections/isConstructor.md index ae7b7ff4..f1981ccc 100644 --- a/docs/archive/current/packages/support/reflections/isConstructor.md +++ b/docs/archive/current/packages/support/reflections/isConstructor.md @@ -23,3 +23,5 @@ 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_). + +See also [`isClassConstructor()`](./isClassConstructor.md). \ No newline at end of file From 0ada395abda0ffb8b04c89b21892f3afe144dc9e Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 15:22:26 +0100 Subject: [PATCH 046/224] Mark isCallable() as stable Until proposal gets through (hopefully), then this implementation will have to do. --- packages/support/src/reflections/isCallable.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/support/src/reflections/isCallable.ts b/packages/support/src/reflections/isCallable.ts index b7cd991c..313f7f34 100644 --- a/packages/support/src/reflections/isCallable.ts +++ b/packages/support/src/reflections/isCallable.ts @@ -1,24 +1,20 @@ import { isClassConstructor } from "./isClassConstructor"; /** - * **WARNING**: _Method is currently unsafe to use! It is subject for breaking changes or possible removal._ - * - * Determine if given argument is callable, but is not a class constructor + * Determine if given argument is callable, but is not a class constructor (es6 style) * * @see {isClassConstructor} * @see https://github.com/caitp/TC39-Proposals/blob/trunk/tc39-reflect-isconstructor-iscallable.md * - * @param {unknown} argument + * @param {unknown} value * * @return {boolean} */ -export function isCallable(argument: unknown): boolean +export function isCallable(value: unknown): boolean { - // TODO: WARNING: Method is currently unsafe to use! It is subject for breaking changes or possible removal. - // Source is heavily inspired by Denis Pushkarev's Core-js implementation of // Function.isCallable / Function.isConstructor, License MIT // @see https://github.com/zloirock/core-js#function-iscallable-isconstructor- - return typeof argument == 'function' && !isClassConstructor(argument); + return typeof value == 'function' && !isClassConstructor(value); } \ No newline at end of file From c8e840b3e8ecead1691235d9d7b54f6975d4001a Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 15:22:34 +0100 Subject: [PATCH 047/224] Improve desc. --- packages/support/src/reflections/isClassConstructor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/reflections/isClassConstructor.ts b/packages/support/src/reflections/isClassConstructor.ts index e3d535b1..06c31747 100644 --- a/packages/support/src/reflections/isClassConstructor.ts +++ b/packages/support/src/reflections/isClassConstructor.ts @@ -1,5 +1,5 @@ /** - * Determine if given value is a class constructor (es6) + * Determine if given value is a class constructor (es6 style) * * @see https://github.com/caitp/TC39-Proposals/blob/trunk/tc39-reflect-isconstructor-iscallable.md * From 87a6e6aeed194b479d2f69206deaa5ee7ee46274 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 15:32:29 +0100 Subject: [PATCH 048/224] Extract isCallable tests into own file Also renamed the previous test file to isConstructor --- .../support/reflections/isCallable.test.js | 35 +++++++++++++++++++ ...structor.test.js => isConstructor.test.js} | 31 +--------------- 2 files changed, 36 insertions(+), 30 deletions(-) create mode 100644 tests/browser/packages/support/reflections/isCallable.test.js rename tests/browser/packages/support/reflections/{isCallable-isConstructor.test.js => isConstructor.test.js} (61%) diff --git a/tests/browser/packages/support/reflections/isCallable.test.js b/tests/browser/packages/support/reflections/isCallable.test.js new file mode 100644 index 00000000..1363edbd --- /dev/null +++ b/tests/browser/packages/support/reflections/isCallable.test.js @@ -0,0 +1,35 @@ +import { + isCallable, +} from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + describe('isCallable', () => { + + it('can determine if is callable', () => { + + class A {} + + const data = [ + { value: undefined, expected: false, name: 'undefined' }, + { value: null, expected: false, name: 'null' }, + { value: {}, expected: false, name: 'object' }, + { value: [], expected: false, name: 'array' }, + { value: A, expected: false, name: 'class' }, + { value: class {}, expected: false, name: 'class (anonymous)' }, + + { value: Array, expected: true, name: 'array (object)' }, + { value: function() {}, expected: true, name: 'function' }, + { value: () => {}, expected: true, name: 'function (arrow)' }, + ]; + + data.forEach((entry, index) => { + + let result = isCallable(entry.value); + expect(result) + .withContext(`${entry.name} was expected to be ${entry.expected}`) + .toBe(entry.expected); + }); + }); + + }); +}); \ No newline at end of file diff --git a/tests/browser/packages/support/reflections/isCallable-isConstructor.test.js b/tests/browser/packages/support/reflections/isConstructor.test.js similarity index 61% rename from tests/browser/packages/support/reflections/isCallable-isConstructor.test.js rename to tests/browser/packages/support/reflections/isConstructor.test.js index 1c18e797..1171c09d 100644 --- a/tests/browser/packages/support/reflections/isCallable-isConstructor.test.js +++ b/tests/browser/packages/support/reflections/isConstructor.test.js @@ -1,12 +1,10 @@ import { - isCallable, - isClassConstructor, isConstructor } from "@aedart/support/reflections"; describe('@aedart/support/reflections', () => { - describe('isConstructor', () => { + describe('isConstructor()', () => { it('can determine if is constructor', () => { @@ -46,31 +44,4 @@ describe('@aedart/support/reflections', () => { }); }); - - describe('isCallable', () => { - - // TODO: Unsafe / Unstable... - - it('can determine if is callable', () => { - - const data = [ - { value: null, expected: false }, - { value: {}, expected: false }, - { value: [], expected: false }, - { value: function() {}, expected: true }, - { value: () => {}, expected: true }, - { value: Array, expected: true }, - { value: class {}, expected: false }, - ]; - - data.forEach((entry, index) => { - - let result = isCallable(entry.value); - expect(result) - .withContext(`Value at index ${index} was expected to be ${entry.expected}`) - .toBe(entry.expected); - }); - }); - - }); }); \ No newline at end of file From 1d5f28a095fbaf3e22f65ca1e99fafd1c4a61d9f Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 15:37:15 +0100 Subject: [PATCH 049/224] Add docs for isCallable --- docs/.vuepress/archive/Version0x.ts | 1 + .../support/reflections/isCallable.md | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 docs/archive/current/packages/support/reflections/isCallable.md diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index deb8fba1..66ddaf80 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -141,6 +141,7 @@ export default PagesCollection.make('v0.x', '/v0x', [ 'packages/support/reflections/hasAllMethods', 'packages/support/reflections/hasMethod', 'packages/support/reflections/hasPrototypeProperty', + 'packages/support/reflections/isCallable', 'packages/support/reflections/isClassConstructor', 'packages/support/reflections/isClassMethodReference', 'packages/support/reflections/isConstructor', diff --git a/docs/archive/current/packages/support/reflections/isCallable.md b/docs/archive/current/packages/support/reflections/isCallable.md new file mode 100644 index 00000000..6dd29509 --- /dev/null +++ b/docs/archive/current/packages/support/reflections/isCallable.md @@ -0,0 +1,29 @@ +--- +title: Is Callable +description: Determine if value is callable. +sidebarDepth: 0 +--- + +# `isCallable` + +Determine if a value is "callable" - a function that is not a [class constructor](./isClassConstructor.md). + +```js +import { isCallable } from "@aedart/support/reflections"; + +isCallable(null); // false +isCallable({}); // false +isCallable([]); // false +isCallable(class {}); // false + +isCallable(function() {}); // true +isCallable(() => {}); // true +isCallable(Array); // 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_). + +See also [`isClassConstructor()`](./isClassConstructor.md). \ No newline at end of file From 4eb5749770ebd806806ba2b58bc621b4a0e9550b Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 16:06:07 +0100 Subject: [PATCH 050/224] Add additional cases for built-in classes / objects --- .../support/reflections/isConstructor.test.js | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/tests/browser/packages/support/reflections/isConstructor.test.js b/tests/browser/packages/support/reflections/isConstructor.test.js index 1171c09d..c7ebd846 100644 --- a/tests/browser/packages/support/reflections/isConstructor.test.js +++ b/tests/browser/packages/support/reflections/isConstructor.test.js @@ -21,24 +21,35 @@ describe('@aedart/support/reflections', () => { } const data = [ - { value: null, expected: false }, - { value: undefined, expected: false }, - { value: /./, expected: false }, - { value: {}, expected: false }, - { value: [], expected: false }, - { value: function() {}, expected: true }, - { value: () => {}, expected: false }, - { value: Array, expected: true }, - { value: class {}, expected: true }, - { value: classWithConstructor, expected: true }, - { value: classWithStaticMethod.foo, expected: false }, + { value: undefined, expected: false, name: 'undefined' }, + { value: null, expected: false, name: 'null' }, + { value: /./, expected: false, name: 'RegExp (as string)' }, + { value: {}, expected: false, name: 'object' }, + { value: [], expected: false, name: 'array' }, + { value: () => {}, expected: false, name: 'function (arrow)' }, + + { value: function() {}, expected: true, name: 'function' }, + { value: Array, expected: true, name: 'Array (object)' }, + { value: String, expected: true, name: 'String (object)' }, + { value: Number, expected: true, name: 'Number (object)' }, + { value: Date, expected: true, name: 'Date (object)' }, + { value: RegExp, expected: true, name: 'RegExp (object)' }, + { value: Map, expected: true, name: 'Map (object)' }, + { value: Set, expected: true, name: 'Set (object)' }, + { value: WeakMap, expected: true, name: 'WeakMap (object)' }, + { value: WeakSet, expected: true, name: 'WeakSet (object)' }, + { value: WeakRef, expected: true, name: 'WeakRef (object)' }, + + { value: classWithConstructor, expected: true, name: 'class' }, + { value: class {}, expected: true, name: 'class (anonymous)' }, + { value: classWithStaticMethod.foo, expected: false, name: 'static class method' }, ]; data.forEach((entry, index) => { let result = isConstructor(entry.value); expect(result) - .withContext(`Value at index ${index} was expected to be ${entry.expected}`) + .withContext(`${entry.name} was expected to be ${entry.expected}`) .toBe(entry.expected); }); }); From 83cb52e74e4cbdf82ef0720c6cfbd46f52c08f31 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 16:06:36 +0100 Subject: [PATCH 051/224] Fix example and improve description of util function --- .../packages/support/reflections/isClassConstructor.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/archive/current/packages/support/reflections/isClassConstructor.md b/docs/archive/current/packages/support/reflections/isClassConstructor.md index f156d31e..01b46eee 100644 --- a/docs/archive/current/packages/support/reflections/isClassConstructor.md +++ b/docs/archive/current/packages/support/reflections/isClassConstructor.md @@ -12,6 +12,10 @@ The `isClassConstructor()` is able to determine if a value is a class constructo `isClassConstructor()` will only be able to return `true` for classes that are defined using the `class` keyword. See [ES6 classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) for additional information. ::: +::: warning Built-in Classes +This util is **NOT** able to detect [built-in classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects). +Use [`isConstructor()`](./isConstructor.md) if you wish to test for "constructable" functions / classes, including built-in classes. +::: ```js import { isClassConstructor } from "@aedart/support/reflections"; @@ -19,10 +23,12 @@ import { isClassConstructor } from "@aedart/support/reflections"; isClassConstructor(null); // false isClassConstructor({}); // false isClassConstructor([]); // false -isClassConstructor(function() {}); // true +isClassConstructor(function() {}); // false isClassConstructor(() => {}); // false isClassConstructor(Array); // false +class A {} +isClassConstructor(A); // true isClassConstructor(class {}); // true ``` From 6c6c71242a37488e5f39e91fcefe4a2f92e202a5 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 16:08:04 +0100 Subject: [PATCH 052/224] Improve shown examples --- .../support/reflections/isConstructor.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/archive/current/packages/support/reflections/isConstructor.md b/docs/archive/current/packages/support/reflections/isConstructor.md index f1981ccc..acd9080e 100644 --- a/docs/archive/current/packages/support/reflections/isConstructor.md +++ b/docs/archive/current/packages/support/reflections/isConstructor.md @@ -6,18 +6,27 @@ 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. +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 value is a constructor. -```js{6,8-9} +```js import { isConstructor } from "@aedart/support/reflections"; isConstructor(null); // false isConstructor({}); // false isConstructor([]); // false -isConstructor(function() {}); // true isConstructor(() => {}); // false -isConstructor(Array); // true + +isConstructor(function() {}); // true isConstructor(class {}); // true + +// Built-in objects +isConstructor(Array); // true +isConstructor(String); // true +isConstructor(Number); // true +isConstructor(Date); // true +isConstructor(Map); // true +isConstructor(Set); // true +// ...etc ``` **Acknowledgement** From de56a26414c82f15c087fa28a9cf887d0aec0283 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 16:08:20 +0100 Subject: [PATCH 053/224] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6639e55b..ff486c5a 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 * Decorator return types for `meta()`, `targetMeta()`, and `inheritTargetMeta()` (_continued to cause TS1270 and TS1238 errors_). [#8](https://github.com/aedart/ion/pull/8), [#9](https://github.com/aedart/ion/pull/9). * Refactored `hasAllMethods()` to use new `isMethod()` internally, in `@aedart/support/reflections`. * Refactored all components that used deprecated `ConstructorOrAbstractConstructor` to use new `ConstructorLike` type alias. +* Marked `isClassConstructor()` and `isCallable()` as stable, in `@aedart/support/reflections`. ### Fixed From 8f394e569433806df213b1424cc2520fbbff90fa Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 16:18:30 +0100 Subject: [PATCH 054/224] Add callback type alias Should cover general cases, but might not always be good enough. --- packages/contracts/src/types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/contracts/src/types.ts b/packages/contracts/src/types.ts index 3da771a1..12e5128a 100644 --- a/packages/contracts/src/types.ts +++ b/packages/contracts/src/types.ts @@ -7,6 +7,11 @@ export * from './decorators'; */ export type Primitive = null | undefined | boolean | number | bigint | string | symbol; +/** + * Callback type + */ +export type Callback = (...args: any[]) => any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + /** * Constructor type */ From edde2987de7af0c77b2bac0f2e514d88323304e1 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 16:18:37 +0100 Subject: [PATCH 055/224] Change release notes --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff486c5a..b11b48d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `dependsOn()`, `dependencies()`, `hasDependencies()`, and `getDependencies()`, in `@aedart/support/container`. * `ClassMethodName` and `ClassMethodReference` type aliases in `@aedart/contracts`. * `isMethod()` util in `@aedart/support/reflections`. -* `ConstructorLike` type alias, in `@aedart/constracts`. +* `ConstructorLike` and `Callback` type aliases, in `@aedart/constracts`. * Add upgrade guide from v0.7.x- to v0.10.x. ### Changed From 3c2c7580a315b3149345fa901206dd6407d047ed Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 16:22:12 +0100 Subject: [PATCH 056/224] Add support for Callback type in call() --- packages/contracts/src/container/Container.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index d634ff45..c9bc8062 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -1,4 +1,5 @@ import { + Callback, Constructor, ClassMethodReference } from "@aedart/contracts"; @@ -179,7 +180,7 @@ export default interface Container build(concrete: Constructor | Binding): T; // TODO: ... - call(method: ClassMethodReference, args: any[]): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + call(method: Callback | ClassMethodReference, args: any[]): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ /** * Forget binding and resolved instance for given identifier From e0f4abfc1b5e9748e43be7731f439659cb8acf04 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 16:59:45 +0100 Subject: [PATCH 057/224] Add CallbackWrapper interface --- .../contracts/src/support/CallbackWrapper.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 packages/contracts/src/support/CallbackWrapper.ts diff --git a/packages/contracts/src/support/CallbackWrapper.ts b/packages/contracts/src/support/CallbackWrapper.ts new file mode 100644 index 00000000..ae05da12 --- /dev/null +++ b/packages/contracts/src/support/CallbackWrapper.ts @@ -0,0 +1,70 @@ +import { Callback } from "@aedart/contracts"; + +/** + * Callback Wrapper + */ +export default interface CallbackWrapper +{ + /** + * The callback + * + * @type {Callback} + * + * @readonly + */ + readonly callback: Callback; + + /** + * "This" value that callback is bound to + * + * @type {object | undefined} + * + * @readonly + */ + readonly binding: object | undefined; + + /** + * Arguments to be passed on to the callback + * when invoked. + * + * @type {any[]} + */ + arguments: any[]; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + /** + * Add arguments to be passed on to the callback + * when it is invoked. + * + * @param {...any} args + * + * @return {this} + */ + with(...args: any[]): this; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + /** + * Determine if callback has any arguments + * + * @return {boolean} + */ + hasArguments(): boolean; + + /** + * Bind callback to given "this" value + * + * @param {object} thisArg + * + * @return {this} + * + * @throws {TypeError} + */ + bind(thisArg: object): this; + + /** + * Invoke the callback + * + * @return {any} + * + * @throws {Error} + */ + call(): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ +} \ No newline at end of file From 862b8b100bf426b34d2fcff6b7e81d001374d9e4 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 16:59:51 +0100 Subject: [PATCH 058/224] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b11b48d2..2aa24f21 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 * `ClassMethodName` and `ClassMethodReference` type aliases in `@aedart/contracts`. * `isMethod()` util in `@aedart/support/reflections`. * `ConstructorLike` and `Callback` type aliases, in `@aedart/constracts`. +* `CallbackWrapper` interface, in `@aedart/constracts/support`. * Add upgrade guide from v0.7.x- to v0.10.x. ### Changed From 30a096211b62f2e5a2bc9e57446a0029adf36137 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 17:00:27 +0100 Subject: [PATCH 059/224] Export CallbackWrapper interface --- packages/contracts/src/support/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/support/index.ts b/packages/contracts/src/support/index.ts index c78f78ef..91508472 100644 --- a/packages/contracts/src/support/index.ts +++ b/packages/contracts/src/support/index.ts @@ -1,5 +1,3 @@ -import Arrayable from "./Arrayable"; - /** * Support identifier * @@ -7,8 +5,11 @@ import Arrayable from "./Arrayable"; */ export const SUPPORT: unique symbol = Symbol('@aedart/contracts/support'); +import Arrayable from "./Arrayable"; +import CallbackWrapper from "./CallbackWrapper"; export { - type Arrayable + type Arrayable, + type CallbackWrapper } export type * from './types'; \ No newline at end of file From 6b7787916516f58fe3efa44031ee9cce6a08ac71 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 11 Mar 2024 17:04:50 +0100 Subject: [PATCH 060/224] Add support for calling a CallbackWrapper instance --- packages/contracts/src/container/Container.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index c9bc8062..c1b281b2 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -3,6 +3,7 @@ import { Constructor, ClassMethodReference } from "@aedart/contracts"; +import { CallbackWrapper } from "@aedart/contracts/support"; import { Alias, Identifier, @@ -180,7 +181,7 @@ export default interface Container build(concrete: Constructor | Binding): T; // TODO: ... - call(method: Callback | ClassMethodReference, args: any[]): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + call(method: Callback | CallbackWrapper | ClassMethodReference, args: any[]): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ /** * Forget binding and resolved instance for given identifier From 36ef5363add9c2830110ed51bab6f4517822a5d4 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 09:09:42 +0100 Subject: [PATCH 061/224] Change wrapper, add hasBinding() util method --- packages/contracts/src/support/CallbackWrapper.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/contracts/src/support/CallbackWrapper.ts b/packages/contracts/src/support/CallbackWrapper.ts index ae05da12..700d50ac 100644 --- a/packages/contracts/src/support/CallbackWrapper.ts +++ b/packages/contracts/src/support/CallbackWrapper.ts @@ -58,6 +58,13 @@ export default interface CallbackWrapper * @throws {TypeError} */ bind(thisArg: object): this; + + /** + * Determine if a binding has been set + * + * @return {boolean} + */ + hasBinding(): boolean; /** * Invoke the callback From 1bf0c6a72da5c809a5c3a9eaaabd0f35460896ad Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 09:14:03 +0100 Subject: [PATCH 062/224] Add CallbackWrapper (implementation --- packages/support/src/CallbackWrapper.ts | 193 ++++++++++++++++++++++++ packages/support/src/index.ts | 7 +- 2 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 packages/support/src/CallbackWrapper.ts diff --git a/packages/support/src/CallbackWrapper.ts b/packages/support/src/CallbackWrapper.ts new file mode 100644 index 00000000..58e5361a --- /dev/null +++ b/packages/support/src/CallbackWrapper.ts @@ -0,0 +1,193 @@ +import type { Callback } from "@aedart/contracts"; +import type { CallbackWrapper as CallbackWrapperInterface } from "@aedart/contracts/support"; + +/** + * Callback Wrapper + * + * @see [CallbackWrapper]{@link import('@aedart/contracts/support').CallbackWrapper} + */ +export default class CallbackWrapper implements CallbackWrapperInterface +{ + /** + * The callback + * + * @type {Callback} + * + * @private + * @readonly + */ + readonly #callback: Callback; + + /** + * "This" value that callback is bound to + * + * @type {object | undefined} + * + * @readonly + * @private + */ + #binding: object | undefined = undefined; + + /** + * Arguments to be passed on to the callback + * when invoked. + * + * @type {any[]} + * + * @private + */ + #arguments: any[] = []; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + /** + * Create a new Callback Wrapper instance + * + * @param {Callback} callback + * @param {...any} [args] + * + * @throws {TypeError} + */ + constructor( + callback: Callback, + ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ) { + if (typeof callback != 'function') { + throw new TypeError('Callback argument must be a valid callable function'); + } + + this.#callback = callback; + this.with(...args); + } + + /** + * Create a new Callback Wrapper instance + * + * @param {Callback} callback + * @param {...any} [args] + * + * @return {this|CallbackWrapper} + * + * @throws {TypeError} + */ + public static make( + callback: Callback, + ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): CallbackWrapperInterface + { + return new this(callback, ...args); + } + + /** + * The callback + * + * @type {Callback} + * + * @readonly + */ + public get callback(): Callback + { + return this.#callback; + } + + /** + * "This" value that callback is bound to + * + * @type {object | undefined} + * + * @readonly + */ + public get binding(): object | undefined + { + return this.#binding; + } + + /** + * Arguments to be passed on to the callback + * when invoked. + * + * @param {any[]} args + */ + public set arguments(args: any[]) /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + this.#arguments = args; + } + + /** + * Arguments to be passed on to the callback + * when invoked. + * + * @return {any[]} + */ + public get arguments(): any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + return this.#arguments; + } + + /** + * Add arguments to be passed on to the callback + * when it is invoked. + * + * @param {...any} args + * + * @return {this} + */ + public with(...args: any[]): this /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + this.arguments = this.arguments.concat(...args); + + return this; + } + + /** + * Determine if callback has any arguments + * + * @return {boolean} + */ + public hasArguments(): boolean + { + return this.arguments.length !== 0; + } + + /** + * Bind callback to given "this" value + * + * @param {object} thisArg + * + * @return {this} + * + * @throws {TypeError} + */ + public bind(thisArg: object): this + { + this.#binding = thisArg; + + return this; + } + + /** + * Determine if a binding has been set + * + * @return {boolean} + */ + hasBinding(): boolean + { + return this.binding !== undefined && this.binding !== null; + } + + /** + * Invoke the callback + * + * @return {any} + * + * @throws {Error} + */ + public call(): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + const callback: Callback = this.callback; + + if (this.hasBinding()) { + return callback.call(this.binding, ...this.arguments); + } + + return callback(...this.arguments); + } +} \ No newline at end of file diff --git a/packages/support/src/index.ts b/packages/support/src/index.ts index 2486adb2..21503de7 100644 --- a/packages/support/src/index.ts +++ b/packages/support/src/index.ts @@ -3,4 +3,9 @@ * * @type {Symbol} */ -export const SUPPORT: unique symbol = Symbol('@aedart/support'); \ No newline at end of file +export const SUPPORT: unique symbol = Symbol('@aedart/support'); + +import CallbackWrapper from "./CallbackWrapper"; +export { + CallbackWrapper +} \ No newline at end of file From 47d0603727ced7a9e101b72b7774f193fd0a64bd Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 09:14:09 +0100 Subject: [PATCH 063/224] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aa24f21..461fba6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `isMethod()` util in `@aedart/support/reflections`. * `ConstructorLike` and `Callback` type aliases, in `@aedart/constracts`. * `CallbackWrapper` interface, in `@aedart/constracts/support`. +* `CallbackWrapper` util class, in `@aedart/support`. * Add upgrade guide from v0.7.x- to v0.10.x. ### Changed From 1a64695c5385ef3620059c24c83b52fc15c8c29d Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 09:34:10 +0100 Subject: [PATCH 064/224] Shorten error message --- packages/support/src/CallbackWrapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/CallbackWrapper.ts b/packages/support/src/CallbackWrapper.ts index 58e5361a..a82c02e6 100644 --- a/packages/support/src/CallbackWrapper.ts +++ b/packages/support/src/CallbackWrapper.ts @@ -51,7 +51,7 @@ export default class CallbackWrapper implements CallbackWrapperInterface ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ ) { if (typeof callback != 'function') { - throw new TypeError('Callback argument must be a valid callable function'); + throw new TypeError('Argument must be a valid callable function'); } this.#callback = callback; From 235232bed93d28912ef41ed14f790fea0783f55c Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 09:48:37 +0100 Subject: [PATCH 065/224] Add static makeFor() method --- packages/support/src/CallbackWrapper.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/support/src/CallbackWrapper.ts b/packages/support/src/CallbackWrapper.ts index a82c02e6..d19f1e71 100644 --- a/packages/support/src/CallbackWrapper.ts +++ b/packages/support/src/CallbackWrapper.ts @@ -75,6 +75,26 @@ export default class CallbackWrapper implements CallbackWrapperInterface { return new this(callback, ...args); } + + /** + * Create a new Callback Wrapper instance, using given binding + * + * @param {object} thisArg Binding + * @param {Callback} callback + * @param {...any} [args] + * + * @return {this|CallbackWrapper} + * + * @throws {TypeError} + */ + public static makeFor( + thisArg: object, + callback: Callback, + ...args: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): CallbackWrapperInterface + { + return this.make(callback, ...args).bind(thisArg); + } /** * The callback From 3ef86a313d62cd52ad7371ce77688990697ec77c Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 10:09:36 +0100 Subject: [PATCH 066/224] Add tests for Callback Wrapper --- .../packages/support/CallbackWrapper.test.js | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 tests/browser/packages/support/CallbackWrapper.test.js diff --git a/tests/browser/packages/support/CallbackWrapper.test.js b/tests/browser/packages/support/CallbackWrapper.test.js new file mode 100644 index 00000000..e3fa060a --- /dev/null +++ b/tests/browser/packages/support/CallbackWrapper.test.js @@ -0,0 +1,161 @@ +import { CallbackWrapper } from "@aedart/support"; + +describe('@aedart/support', () => { + describe('CallbackWrapper', () => { + + it('fails if callback argument is not callable', () => { + + const callback = () => { + CallbackWrapper.make(null); + } + + expect(callback) + .toThrowError(TypeError); + }); + + it('can wrap callback with arguments', () => { + + const callback = () => { + return true; + } + + const args = [ 'a', 'b', 'c' ]; + + // -------------------------------------------------------------------- // + + const wrapper = CallbackWrapper.make(callback, ...args); + + expect(wrapper.callback) + .withContext('Invalid callback function in wrapper') + .toBe(callback); + + expect(wrapper.hasArguments()) + .withContext('No arguments in wrapper') + .toBeTrue(); + + expect(wrapper.arguments) + .withContext('Invalid arguments in wrapper') + .toEqual(args); + }); + + it('can add additional arguments', () => { + const callback = () => { + return true; + } + + const initialArgs = [ 'a', 'b', 'c' ]; + const additional = [ 'd', 'e', 'f' ]; + + // -------------------------------------------------------------------- // + + const wrapper = CallbackWrapper.make(callback, ...initialArgs) + .with(...additional); + + expect(wrapper.hasArguments()) + .withContext('No arguments in wrapper') + .toBeTrue(); + + expect(wrapper.arguments) + .withContext('Invalid arguments in wrapper') + .toEqual([ ...initialArgs, ...additional ]); + }); + + it('can clear arguments', () => { + const callback = () => { + return true; + } + + const args = [ 'a', 'b', 'c' ]; + + // -------------------------------------------------------------------- // + + const wrapper = CallbackWrapper.make(callback, ...args); + wrapper.arguments = []; + + expect(wrapper.hasArguments()) + .withContext('Arguments not cleared in wrapper') + .toBeFalse(); + }); + + it('can specify a binding', () => { + const callback = () => { + return true; + } + + class A {} + const instance = new A(); + + // -------------------------------------------------------------------- // + + const wrapper = CallbackWrapper.makeFor(instance, callback); + + expect(wrapper.hasBinding()) + .withContext('No binding in wrapper') + .toBeTrue(); + + expect(wrapper.binding) + .withContext('Incorrect binding in wrapper') + .toBe(instance); + }); + + it('can invoke callback', () => { + + const value = 1234; + const callback = () => { + return value; + } + + // -------------------------------------------------------------------- // + + const wrapper = CallbackWrapper.make(callback); + const result = wrapper.call(); + + expect(result) + .withContext('Invalid callback result') + .toBe(value); + }); + + it('can invoke callback with arguments', () => { + + const callback = (arg1, arg2) => { + return arg1 + arg2; + } + + const a = 1; + const b = 3; + + // -------------------------------------------------------------------- // + + const wrapper = CallbackWrapper.make(callback, a, b); + const result = wrapper.call(); + + expect(result) + .withContext('Invalid callback result') + .toBe(a + b); + }); + + it('can invoke callback with binding', () => { + + const callback = function(name) { + return this.sayHi(name); + }; + + class A { + sayHi(name) { + return `Hi ${name}`; + } + } + + const name = 'Olivia'; + + // -------------------------------------------------------------------- // + + const wrapper = CallbackWrapper.makeFor(new A(), callback, name); + const result = wrapper.call(); + + expect(result) + .withContext('Invalid callback result') + .toEqual(`Hi ${name}`); + }); + }); +}); \ No newline at end of file From 358e67b21908cf7017c2fcdccccc33545185f00e Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 10:27:41 +0100 Subject: [PATCH 067/224] Add isCallbackWrapper() util --- packages/support/src/index.ts | 4 +- packages/support/src/isCallbackWrapper.ts | 39 +++++++++++++++++++ .../support/isCallbackWrapper.test.js | 38 ++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 packages/support/src/isCallbackWrapper.ts create mode 100644 tests/browser/packages/support/isCallbackWrapper.test.js diff --git a/packages/support/src/index.ts b/packages/support/src/index.ts index 21503de7..d4ec2f53 100644 --- a/packages/support/src/index.ts +++ b/packages/support/src/index.ts @@ -8,4 +8,6 @@ export const SUPPORT: unique symbol = Symbol('@aedart/support'); import CallbackWrapper from "./CallbackWrapper"; export { CallbackWrapper -} \ No newline at end of file +} + +export * from './isCallbackWrapper'; \ No newline at end of file diff --git a/packages/support/src/isCallbackWrapper.ts b/packages/support/src/isCallbackWrapper.ts new file mode 100644 index 00000000..83e62592 --- /dev/null +++ b/packages/support/src/isCallbackWrapper.ts @@ -0,0 +1,39 @@ +import CallbackWrapper from "./CallbackWrapper"; + +/** + * Determine if given value is a [CallbackWrapper]{@link import('@aedart/contracts/support').CallbackWrapper} + * + * @param {unknown} value + * + * @return {boolean} + */ +export function isCallbackWrapper(value: unknown): boolean +{ + if (!value || typeof value != 'object') { + return false; + } + + if (value instanceof CallbackWrapper) { + return true; + } + + // Determine if value "looks like" a callback wrapper object + const blueprint: PropertyKey[] = [ + 'callback', + 'binding', + 'arguments', + 'with', + 'hasArguments', + 'bind', + 'hasBinding', + 'call' + ]; + + for (const property of blueprint) { + if (!Reflect.has(value, property)) { + return false; + } + } + + return true; +} \ No newline at end of file diff --git a/tests/browser/packages/support/isCallbackWrapper.test.js b/tests/browser/packages/support/isCallbackWrapper.test.js new file mode 100644 index 00000000..b250af4e --- /dev/null +++ b/tests/browser/packages/support/isCallbackWrapper.test.js @@ -0,0 +1,38 @@ +import { CallbackWrapper, isCallbackWrapper } from "@aedart/support"; + +describe('@aedart/support', () => { + describe('isCallbackWrapper', () => { + + it('can determine if value is a callback wrapper object', () => { + + // Custom Callback Wrapper - valid because it contains all expected property keys, + // despite not truly satisfies the interface... + const custom = { + 'callback': false, + 'binding': false, + 'arguments': false, + 'with': false, + 'hasArguments': false, + 'bind': false, + 'hasBinding': false, + 'call': false, + }; + + const data = [ + { value: undefined, expected: false, name: 'undefined' }, + { value: null, expected: false, name: 'null' }, + { value: [], expected: false, name: 'array' }, + { value: {}, expected: false, name: 'object' }, + + { value: CallbackWrapper.make(() => true), expected: true, name: 'CallbackWrapper (instance)' }, + { value: custom, expected: true, name: 'custom CallbackWrapper (instance)' }, + ]; + + for (const entry of data) { + expect(isCallbackWrapper(entry.value)) + .withContext(`${entry.name} was expected to ${entry.expected.toString()}`) + .toBe(entry.expected); + } + }); + }); +}); \ No newline at end of file From f24e6ac93e6f60c4a75bcce8a2fb2917646d7f5e Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 10:27:48 +0100 Subject: [PATCH 068/224] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 461fba6e..54554a9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `ConstructorLike` and `Callback` type aliases, in `@aedart/constracts`. * `CallbackWrapper` interface, in `@aedart/constracts/support`. * `CallbackWrapper` util class, in `@aedart/support`. +* `isCallbackWrapper` util, in `@aedart/support`. * Add upgrade guide from v0.7.x- to v0.10.x. ### Changed From eed185af06ab573c33dcbcdf139fe0bdcbeaf1ce Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 10:40:45 +0100 Subject: [PATCH 069/224] Add case for calling arrow function with binding --- .../packages/support/CallbackWrapper.test.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/browser/packages/support/CallbackWrapper.test.js b/tests/browser/packages/support/CallbackWrapper.test.js index e3fa060a..50fad0ea 100644 --- a/tests/browser/packages/support/CallbackWrapper.test.js +++ b/tests/browser/packages/support/CallbackWrapper.test.js @@ -157,5 +157,29 @@ describe('@aedart/support', () => { .withContext('Invalid callback result') .toEqual(`Hi ${name}`); }); + + it('fails invoking callback if unable to apply binding', () => { + + // NOTE: Unable to "bind" arrow functions! + const callback = (name) => { + return this.sayHi(name); + }; + + class A { + sayHi(name) { + return `Hi ${name}`; + } + } + + // -------------------------------------------------------------------- // + + const wrapper = CallbackWrapper.makeFor(new A(), callback, 'Bart'); + const invoke = () => { + wrapper.call() + } + + expect(invoke) + .toThrowError(TypeError); + }); }); }); \ No newline at end of file From 2476af8f636d9e60485f907884481ddc543165d6 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 11:22:24 +0100 Subject: [PATCH 070/224] Add docs for callback wrapper --- docs/.vuepress/archive/Version0x.ts | 1 + .../packages/support/CallbackWrapper.md | 169 ++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 docs/archive/current/packages/support/CallbackWrapper.md diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index 66ddaf80..df4e4ecc 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -168,6 +168,7 @@ export default PagesCollection.make('v0.x', '/v0x', [ 'packages/support/misc/toWeakRef', ] }, + 'packages/support/CallbackWrapper', ] }, { diff --git a/docs/archive/current/packages/support/CallbackWrapper.md b/docs/archive/current/packages/support/CallbackWrapper.md new file mode 100644 index 00000000..310a1de3 --- /dev/null +++ b/docs/archive/current/packages/support/CallbackWrapper.md @@ -0,0 +1,169 @@ +--- +title: Callback Wrapper +description: Wrapper object for a callback +sidebarDepth: 0 +--- + +# Callback Wrapper + +The `CallbackWrapper` objects offers a convenient way to wrap a callable function. + +```js +import { CallbackWrapper } from "@aedart/support"; + +const wrapped = CallbackWrapper.make(() => { + return 'Hi there...'; +}); + +// Later in your application +wrapped.call(); // Hi there... +``` + +[[TOC]] + +## Call + +The `call()` method invokes the wrapped callback and returns its eventual output. + +```js +const wrapped = CallbackWrapper.make(() => { + return true; +}); + +wrapped.call(); // true +``` + +## Arguments + +There are several ways to specify arguments that must be applied for the wrapped callback, when `call()` is invoked. + +### Via `make()` + +The static `make()` method allows you to specify arguments right away. +This is useful, if you already know the arguments. + +```js +const wrapped = CallbackWrapper.make((firstname, lastname) => { + return `Hi ${firstname} ${lastname}`; +}, 'Timmy', 'Jackson'); + +wrapped.call(); // Hi Timmy Jackson +``` + +### Via `with()` + +In situations when you must add additional arguments, e.g. because you might not know all arguments up front, then you +can use the `with()` method. + +```js +const wrapped = CallbackWrapper.make((firstname, lastname) => { + return `Hi ${firstname} ${lastname}`; +}, 'Siw'); + +wrapped + .with('Orion') + .call(); // Hi Siw Orion +``` + +### Via `arguments` + +Lastly, in situations when you must completely overwrite all arguments, then you can specify them via the `arguments` +property. + +```js +const wrapped = CallbackWrapper.make((firstname, lastname) => { + return `Hi ${firstname} ${lastname}`; +}); + +wrapped.arguments = [ 'Alpha', 'Zero' ]; +wrapped + .call(); // Hi Alpha Zero +``` + +## Binding + +Use `bind()` to specify the callback's [`this` value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind). + +```js +class A { + sayHi(name) { + return `Hi ${name}`; + } +} +const instance = new A(); + +const wrapped = CallbackWrapper.make(function(name) { + return this.sayHi(name); +}); + +wrapped + .bind(instance) + .with('Akari') + .call(); // Hi Akari +``` + +### Binding vs. Arrow Function + +::: warning +It is not possible to apply a binding on an arrow function callback. Doing so can result in a `TypeError` or other unexpected behaviour. +See [Mozilla's documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) for additional information. + +**_:x:_** + +```js +// Callback Wrapper for arrow function... +const wrapped = CallbackWrapper.make(() => { + // ...not shown ... +}); + +wrapped + .bind(myObject) + .call(); // TypeError +``` + +**_:heavy_check_mark:_** + +```js +// Callback Wrapper for normal function... +const wrapped = CallbackWrapper.make(function () { + // ...not shown ... +}); + +wrapped + .bind(myObject) + .call(); +``` +::: + +## Misc. + +If you need to determine if a value is a "callback wrapper" object, then you can use the `isCallbackWrapper()` util. + +```js +import { isCallbackWrapper, CallbackWrapper } from "@aedart/support"; + +isCallbackWrapper(() => true); // false +isCallbackWrapper(CallbackWrapper.make(() => true)); // true +``` + +### Custom Callback Wrapper + +`isCallbackWrapper()` can also accept custom implementation of a callback wrapper. + +```js +// Custom implementation of a callback wrapper +const custom = { + 'callback': function() { /* not shown */ }, + 'binding': undefined, + 'arguments': [], + 'with': function() { /* not shown */ }, + 'hasArguments': function() { /* not shown */ }, + 'bind': function() { /* not shown */ }, + 'hasBinding': function() { /* not shown */ }, + 'call': function() { /* not shown */ }, +}; + +isCallbackWrapper(custom); // true +``` + +_See the source code of `isCallbackWrapper()` for additional details._ \ No newline at end of file From 24830e5e1e1273f21c34bf650edf5ec415b0dd7b Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 11:45:31 +0100 Subject: [PATCH 071/224] Change default value to undefined for D generic --- packages/contracts/src/support/meta/Repository.ts | 9 +++------ .../contracts/src/support/meta/TargetRepository.ts | 9 +++------ packages/support/src/meta/MetaRepository.ts | 9 +++------ .../support/src/meta/target/TargetMetaRepository.ts | 13 +++++-------- 4 files changed, 14 insertions(+), 26 deletions(-) diff --git a/packages/contracts/src/support/meta/Repository.ts b/packages/contracts/src/support/meta/Repository.ts index c5bee737..cb7f53a0 100644 --- a/packages/contracts/src/support/meta/Repository.ts +++ b/packages/contracts/src/support/meta/Repository.ts @@ -37,17 +37,14 @@ export default interface Repository * Get value for given key * * @template T Return value type - * @template D=any Type of default value + * @template D=undefined Type of default value * * @param {Key} key * @param {D} [defaultValue] * - * @return {T | D | undefined} + * @return {T | D} */ - get< - T, - D = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ - >(key: Key, defaultValue?: D): T | D | undefined; + get(key: Key, defaultValue?: D): T | D; /** * Determine if value exists for key diff --git a/packages/contracts/src/support/meta/TargetRepository.ts b/packages/contracts/src/support/meta/TargetRepository.ts index 6ce0a93f..e4f78caf 100644 --- a/packages/contracts/src/support/meta/TargetRepository.ts +++ b/packages/contracts/src/support/meta/TargetRepository.ts @@ -34,18 +34,15 @@ export default interface TargetRepository * Get value for given key * * @template T Return value type - * @template D=any Type of default value + * @template D=undefined Type of default value * * @param {object} target Class or class method target * @param {Key} key * @param {D} [defaultValue] * - * @return {T | D | undefined} + * @return {T | D} */ - get< - T, - D = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ - >(target: object, key: Key, defaultValue?: D): T | D | undefined; + get(target: object, key: Key, defaultValue?: D): T | D; /** * Determine if value exists for key diff --git a/packages/support/src/meta/MetaRepository.ts b/packages/support/src/meta/MetaRepository.ts index 90cc0b85..54684b76 100644 --- a/packages/support/src/meta/MetaRepository.ts +++ b/packages/support/src/meta/MetaRepository.ts @@ -140,17 +140,14 @@ export default class MetaRepository implements Repository * Get value for given key * * @template T Return value type - * @template D=any Type of default value + * @template D=undefined Type of default value * * @param {Key} key * @param {D} [defaultValue] * - * @return {T | D | undefined} + * @return {T | D} */ - public get< - T, - D = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ - >(key: Key, defaultValue?: D): T | D | undefined + public get(key: Key, defaultValue?: D): T | D { return get(this.all(), key, defaultValue); } diff --git a/packages/support/src/meta/target/TargetMetaRepository.ts b/packages/support/src/meta/target/TargetMetaRepository.ts index 74d4b0e6..0bfe4851 100644 --- a/packages/support/src/meta/target/TargetMetaRepository.ts +++ b/packages/support/src/meta/target/TargetMetaRepository.ts @@ -78,31 +78,28 @@ export default class TargetMetaRepository implements TargetRepository * Get value for given key * * @template T Return value type - * @template D=any Type of default value + * @template D=undefined Type of default value * * @param {object} target Class or class method target * @param {Key} key * @param {D} [defaultValue] * - * @return {T | D | undefined} + * @return {T | D} */ - public get< - T, - D = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ - >(target: object, key: Key, defaultValue?: D): T | D | undefined + public get(target: object, key: Key, defaultValue?: D): T | D { // Find "target" meta address for given target object // or return the default value if none is found. const address: MetaAddress | undefined = this.find(target); if (address === undefined) { - return defaultValue; + return defaultValue as D; } // When an address was found, we must ensure that the meta // owner class still exists. If not, return default value. const owner: object | undefined = address[0]?.deref(); if (owner === undefined) { - return defaultValue; + return defaultValue as D; } // Finally, use getMeta to obtain desired key. From 0903df92c32bbd8c8c720dd4e428dfb07f39bcb5 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 11:45:38 +0100 Subject: [PATCH 072/224] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54554a9f..ae76f71f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 **Breaking** * Added `hasAny()` method in `TargetRepository` interface, in `@aedart/contracts/meta`. +* Default generic for `defaultValue` changed to `undefined`, for `get()` methods in meta `Repository` and `TargetRepository`. **Non-breaking Changes** From e00228b713cf1e7052f3c4623386301df0fe9b4c Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 12:26:56 +0100 Subject: [PATCH 073/224] Add ArbitraryData concern --- .../contracts/src/support/HasArbitraryData.ts | 62 ++++++++++ packages/contracts/src/support/index.ts | 4 +- packages/support/src/ArbitraryData.ts | 98 ++++++++++++++++ packages/support/src/index.ts | 2 + .../packages/support/ArbitraryData.test.js | 107 ++++++++++++++++++ 5 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 packages/contracts/src/support/HasArbitraryData.ts create mode 100644 packages/support/src/ArbitraryData.ts create mode 100644 tests/browser/packages/support/ArbitraryData.test.js diff --git a/packages/contracts/src/support/HasArbitraryData.ts b/packages/contracts/src/support/HasArbitraryData.ts new file mode 100644 index 00000000..662e0803 --- /dev/null +++ b/packages/contracts/src/support/HasArbitraryData.ts @@ -0,0 +1,62 @@ +import { Key } from "./types"; + +/** + * Has Arbitrary Data + */ +export default interface HasArbitraryData +{ + /** + * Set value for key + * + * @param {Key} key + * @param {any} value + * + * @return {this} + */ + set(key: Key, value: any): this; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + /** + * Get value for key, or default if key does not exist + * + * @template T + * @template D=undefined + * + * @param {Key} key + * @param {D} [defaultValue] + * + * @return {T | D} + */ + get(key: Key, defaultValue?: D): T | D; + + /** + * Determine if value exists for key + * + * @param {Key} key + * + * @return {boolean} + */ + has(key: Key): boolean; + + /** + * Delete value for key + * + * @param {Key} key + * + * @return {boolean} + */ + forget(key: Key): boolean; + + /** + * Returns all arbitrary data + * + * @return {Record} + */ + all(): Record; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + /** + * Flush all arbitrary data + * + * @return {void} + */ + flush(): void; +} \ No newline at end of file diff --git a/packages/contracts/src/support/index.ts b/packages/contracts/src/support/index.ts index 91508472..157c9b9f 100644 --- a/packages/contracts/src/support/index.ts +++ b/packages/contracts/src/support/index.ts @@ -7,9 +7,11 @@ export const SUPPORT: unique symbol = Symbol('@aedart/contracts/support'); import Arrayable from "./Arrayable"; import CallbackWrapper from "./CallbackWrapper"; +import HasArbitraryData from "./HasArbitraryData"; export { type Arrayable, - type CallbackWrapper + type CallbackWrapper, + type HasArbitraryData } export type * from './types'; \ No newline at end of file diff --git a/packages/support/src/ArbitraryData.ts b/packages/support/src/ArbitraryData.ts new file mode 100644 index 00000000..4b0a9d6b --- /dev/null +++ b/packages/support/src/ArbitraryData.ts @@ -0,0 +1,98 @@ +import {HasArbitraryData, Key} from "@aedart/contracts/support"; +import { AbstractConcern } from "@aedart/support/concerns"; +import { set, get, has, forget, merge } from "@aedart/support/objects"; + +/** + * Concerns Arbitrary Data + * + * @see HasArbitraryData + * + * @mixin + */ +export default class ArbitraryData extends AbstractConcern implements HasArbitraryData +{ + /** + * The arbitrary data record + * + * @type {Record} + * + * @private + */ + #data: Record = {}; + + /** + * Set value for key + * + * @param {Key} key + * @param {any} value + * + * @return {this} + */ + set(key: Key, value: any): this /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + set(this.#data, key, value); + + return this.concernOwner as this; + } + + /** + * Get value for key, or default if key does not exist + * + * @template T + * @template D=undefined + * + * @param {Key} key + * @param {D} [defaultValue] + * + * @return {T | D} + */ + get(key: Key, defaultValue?: D): T | D + { + return get(this.#data, key, defaultValue); + } + + /** + * Determine if value exists for key + * + * @param {Key} key + * + * @return {boolean} + */ + has(key: Key): boolean + { + return has(this.#data, key); + } + + /** + * Delete value for key + * + * @param {Key} key + * + * @return {boolean} + */ + forget(key: Key): boolean + { + return forget(this.#data, key); + } + + /** + * Returns all arbitrary data + * + * @return {Record} + */ + all(): Record /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + // Returns a copy of the arbitrary data record + return merge(this.#data); + } + + /** + * Flush all arbitrary data + * + * @return {void} + */ + flush(): void + { + this.#data = {}; + } +} \ No newline at end of file diff --git a/packages/support/src/index.ts b/packages/support/src/index.ts index d4ec2f53..d139c7cb 100644 --- a/packages/support/src/index.ts +++ b/packages/support/src/index.ts @@ -5,8 +5,10 @@ */ export const SUPPORT: unique symbol = Symbol('@aedart/support'); +import ArbitraryData from "./ArbitraryData"; import CallbackWrapper from "./CallbackWrapper"; export { + ArbitraryData, CallbackWrapper } diff --git a/tests/browser/packages/support/ArbitraryData.test.js b/tests/browser/packages/support/ArbitraryData.test.js new file mode 100644 index 00000000..b120e8b0 --- /dev/null +++ b/tests/browser/packages/support/ArbitraryData.test.js @@ -0,0 +1,107 @@ +import { ArbitraryData } from "@aedart/support"; + +describe('@aedart/support', () => { + describe('ArbitraryData', () => { + + it('can set and obtain value for key', () => { + + const key = 'a.b'; + const value = 'Swell'; + + // -------------------------------------------------------------------- // + + const data = new ArbitraryData(); + data.set(key, value); + + expect(data.has(key)) + .withContext('Key does not appear to exist') + .toBeTrue(); + + expect(data.get(key)) + .withContext('Incorrect value for key') + .toBe(value); + }); + + it('returns default value when key does not exist', () => { + + const key = 'zar'; + const defaultValue = 'my_default'; + + // -------------------------------------------------------------------- // + + const data = new ArbitraryData(); + + expect(data.get(key, defaultValue)) + .withContext('Default value not returned') + .toBe(defaultValue) + }); + + it('can forget key-value pair', () => { + const key = 'foo'; + const value = 'bar'; + + // -------------------------------------------------------------------- // + + const data = new ArbitraryData(); + data.set(key, value); + + const result = data.forget(key); + expect(result) + .withContext('Key not forgotten!') + .toBeTrue(); + + expect(data.has(key)) + .withContext('Key SHOULD NOT exist') + .toBeFalse(); + }); + + it('can flush all arbitrary data', () => { + const keyA = 'foo'; + const keyB = 'bar'; + const keyC = 'zar'; + + // -------------------------------------------------------------------- // + + const data = new ArbitraryData(); + data + .set(keyA, 'a') + .set(keyB, 'b') + .set(keyC, 'c'); + + data.flush(); + + expect(data.has(keyA)) + .withContext('Key A SHOULD NOT exist') + .toBeFalse(); + + expect(data.has(keyB)) + .withContext('Key B SHOULD NOT exist') + .toBeFalse(); + + expect(data.has(keyC)) + .withContext('Key C SHOULD NOT exist') + .toBeFalse(); + }); + + it('can return all arbitrary data (record)', () => { + const keyA = 'foo'; + const keyB = 'bar'; + + // -------------------------------------------------------------------- // + + const data = new ArbitraryData(); + data + .set(keyA, 'a') + .set(keyB, 'b'); + + const expected = { + [keyA]: 'a', + [keyB]: 'b' + }; + + expect(data.all()) + .withContext('Incorrect record') + .toEqual(expected); + }); + }); +}); \ No newline at end of file From fe3cacd7943c1752f71353bf72252eb6548170c3 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 12:37:17 +0100 Subject: [PATCH 074/224] (Re)Fix TS1270 and TS1238 errors Sadly it appears to be "impossible" to use valid return types for decorators. The "any" type seems to be the only acceptable solution... Or at least for now. --- packages/support/src/concerns/use.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/concerns/use.ts b/packages/support/src/concerns/use.ts index f148f38a..7bd62e66 100644 --- a/packages/support/src/concerns/use.ts +++ b/packages/support/src/concerns/use.ts @@ -33,7 +33,7 @@ import ConcernsInjector from "./ConcernsInjector"; * * @throws {InjectionException} */ -export function use(...concerns: (ConcernConstructor|Configuration|ShorthandConfiguration)[]) +export function use(...concerns: (ConcernConstructor|Configuration|ShorthandConfiguration)[]): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { return (target: object) => { return (new ConcernsInjector(target)).inject(...concerns); From 7085852ace7a996f1daad01bd3d51510abaa18b5 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 12:42:14 +0100 Subject: [PATCH 075/224] Fix missing extends JSDoc The "@mixin" / "@mixes" will not work unless @extends is also applied on the concern / mixin. --- packages/support/src/ArbitraryData.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/support/src/ArbitraryData.ts b/packages/support/src/ArbitraryData.ts index 4b0a9d6b..8411b67e 100644 --- a/packages/support/src/ArbitraryData.ts +++ b/packages/support/src/ArbitraryData.ts @@ -8,6 +8,7 @@ import { set, get, has, forget, merge } from "@aedart/support/objects"; * @see HasArbitraryData * * @mixin + * @extends AbstractConcern */ export default class ArbitraryData extends AbstractConcern implements HasArbitraryData { From c846b0a85dd97054d53b37a824644f565a75b28a Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 12:46:01 +0100 Subject: [PATCH 076/224] Cleanup --- packages/support/src/ArbitraryData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/ArbitraryData.ts b/packages/support/src/ArbitraryData.ts index 8411b67e..02abae2b 100644 --- a/packages/support/src/ArbitraryData.ts +++ b/packages/support/src/ArbitraryData.ts @@ -1,4 +1,4 @@ -import {HasArbitraryData, Key} from "@aedart/contracts/support"; +import { HasArbitraryData, Key } from "@aedart/contracts/support"; import { AbstractConcern } from "@aedart/support/concerns"; import { set, get, has, forget, merge } from "@aedart/support/objects"; From 19cb91bc8669ee3d78e251704c5c0675675a9878 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 13:07:13 +0100 Subject: [PATCH 077/224] Cleanup --- packages/support/src/ArbitraryData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/ArbitraryData.ts b/packages/support/src/ArbitraryData.ts index 02abae2b..5a24ea12 100644 --- a/packages/support/src/ArbitraryData.ts +++ b/packages/support/src/ArbitraryData.ts @@ -1,4 +1,4 @@ -import { HasArbitraryData, Key } from "@aedart/contracts/support"; +import type { HasArbitraryData, Key } from "@aedart/contracts/support"; import { AbstractConcern } from "@aedart/support/concerns"; import { set, get, has, forget, merge } from "@aedart/support/objects"; From 3c73b31178c9d035afbe587019d6e76f87aab87a Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 13:36:20 +0100 Subject: [PATCH 078/224] Change CallbackWrapper, use Arbitrary Data concern Note: the JSDoc is VERY cumbersome for now. It appears that TypeScript's decorator helpers strips the class' JSDoc when applying a decorator. In a future version, this can perhaps be reduced to just @mixes tag, but for now this must do. --- packages/support/src/CallbackWrapper.ts | 75 +++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/packages/support/src/CallbackWrapper.ts b/packages/support/src/CallbackWrapper.ts index d19f1e71..c176c445 100644 --- a/packages/support/src/CallbackWrapper.ts +++ b/packages/support/src/CallbackWrapper.ts @@ -1,13 +1,88 @@ import type { Callback } from "@aedart/contracts"; import type { CallbackWrapper as CallbackWrapperInterface } from "@aedart/contracts/support"; +import { use } from "@aedart/support/concerns"; +import ArbitraryData from "./ArbitraryData"; /** * Callback Wrapper * * @see [CallbackWrapper]{@link import('@aedart/contracts/support').CallbackWrapper} + * + * @mixes ArbitraryData */ +@use(ArbitraryData) export default class CallbackWrapper implements CallbackWrapperInterface { + /** + * Alias for {@link ArbitraryData#set} + * + * @function set + * @param {Key} key + * @param {any} value + * @return {this} + * + * @instance + * @memberof CallbackWrapper + */ + + /** + * Alias for {@link ArbitraryData#get} + * + * @function get + * + * @template T + * @template D=undefined + * + * @param {Key} key + * @param {D} [defaultValue] + * @return {this} + * + * @instance + * @memberof CallbackWrapper + */ + + /** + * Alias for {@link ArbitraryData#has} + * + * @function has + * @param {Key} key + * @return {boolean} + * + * @instance + * @memberof CallbackWrapper + */ + + /** + * Alias for {@link ArbitraryData#forget} + * + * @function forget + * @param {Key} key + * @return {boolean} + * + * @instance + * @memberof CallbackWrapper + */ + + /** + * Alias for {@link ArbitraryData#all} + * + * @function all + * @return {Record} + * + * @instance + * @memberof CallbackWrapper + */ + + /** + * Alias for {@link ArbitraryData#flush} + * + * @function flush + * @return {void} + * + * @instance + * @memberof CallbackWrapper + */ + /** * The callback * From 81e5c7685e579cf4bf521268e5c31a7780c5d6d1 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 13:36:43 +0100 Subject: [PATCH 079/224] Add test for Arbitrary Data concern usage --- .../packages/support/CallbackWrapper.test.js | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/browser/packages/support/CallbackWrapper.test.js b/tests/browser/packages/support/CallbackWrapper.test.js index 50fad0ea..d233ab8c 100644 --- a/tests/browser/packages/support/CallbackWrapper.test.js +++ b/tests/browser/packages/support/CallbackWrapper.test.js @@ -1,4 +1,5 @@ -import { CallbackWrapper } from "@aedart/support"; +import { CallbackWrapper, ArbitraryData } from "@aedart/support"; +import { usesConcerns } from "@aedart/support/concerns"; describe('@aedart/support', () => { describe('CallbackWrapper', () => { @@ -181,5 +182,34 @@ describe('@aedart/support', () => { expect(invoke) .toThrowError(TypeError); }); + + it('uses arbitrary data concern', () => { + + const wrapper = CallbackWrapper.make(() => true); + + // -------------------------------------------------------------------- // + + expect(usesConcerns(wrapper, ArbitraryData)) + .withContext('Wrapper does not use Arbitrary Data concern') + .toBeTrue(); + + wrapper + .set('a', 'foo') + .set('b', 'bar'); + + expect(wrapper.has('a')) + .withContext('a key does not exist') + .toBeTrue(); + expect(wrapper.get('a')) + .withContext('a incorrect value') + .toBe('foo'); + + expect(wrapper.has('b')) + .withContext('b key does not exist') + .toBeTrue(); + expect(wrapper.get('b')) + .withContext('b incorrect value') + .toBe('bar'); + }); }); }); \ No newline at end of file From fd87a5c800f40db25666f64eacd19f7c63c11459 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 13:55:10 +0100 Subject: [PATCH 080/224] Fix missing tslib as peer dependency --- packages/support/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/support/package.json b/packages/support/package.json index d826e33f..b6d36d8f 100644 --- a/packages/support/package.json +++ b/packages/support/package.json @@ -81,7 +81,8 @@ "peerDependencies": { "@aedart/contracts": "^0.10.0", "@types/lodash-es": "^4.17.12", - "lodash-es": "^4.17.21" + "lodash-es": "^4.17.21", + "tslib": "^2.6.2" }, "scripts": { "compile": "rollup -c", From 163564592e638fa61584e701523c7cb810e3f5e2 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 13:56:55 +0100 Subject: [PATCH 081/224] Update dependencies --- package-lock.json | 422 +++++++++++++++++++++++++--------------------- package.json | 5 +- 2 files changed, 229 insertions(+), 198 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f166eb5..116a26f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,14 @@ "packages/*" ], "devDependencies": { - "@babel/cli": "^7.23.4", - "@babel/core": "^7.23.7", + "@babel/cli": "^7.23.9", + "@babel/core": "^7.24.0", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-decorators": "^7.23.7", "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-transform-class-static-block": "^7.23.4", "@babel/preset-env": "^7.23.8", + "@babel/runtime": "^7.24.0", "@lerna-lite/changed": "^3.2.1", "@lerna-lite/cli": "^3.2.1", "@lerna-lite/list": "^3.2.1", @@ -286,9 +287,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.0.tgz", + "integrity": "sha512-efwOM90nCG6YeT8o3PCyBVSxRfmILxCNL+TNI8CGQl7a62M0Wd9VkV+XHwIlkOz1r4b+lxu6gBjdWiOMdUCrCQ==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -2524,12 +2525,12 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { @@ -3042,12 +3043,12 @@ "dev": true }, "node_modules/@ljharb/through": { - "version": "2.3.12", - "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.12.tgz", - "integrity": "sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==", + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", + "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.5" + "call-bind": "^1.0.7" }, "engines": { "node": ">= 0.4" @@ -4122,9 +4123,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.1.tgz", - "integrity": "sha512-iU2Sya8hNn1LhsYyf0N+L4Gf9Qc+9eBTJJJsaOGUp+7x4n2M9dxTt8UvhJl3oeftSjblSlpCfvjA/IfP3g5VjQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", + "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", "cpu": [ "arm" ], @@ -4135,9 +4136,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.1.tgz", - "integrity": "sha512-wlzcWiH2Ir7rdMELxFE5vuM7D6TsOcJ2Yw0c3vaBR3VOsJFVTx9xvwnAvhgU5Ii8Gd6+I11qNHwndDscIm0HXg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", + "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", "cpu": [ "arm64" ], @@ -4148,9 +4149,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.1.tgz", - "integrity": "sha512-YRXa1+aZIFN5BaImK+84B3uNK8C6+ynKLPgvn29X9s0LTVCByp54TB7tdSMHDR7GTV39bz1lOmlLDuedgTwwHg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", + "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", "cpu": [ "arm64" ], @@ -4161,9 +4162,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.1.tgz", - "integrity": "sha512-opjWJ4MevxeA8FhlngQWPBOvVWYNPFkq6/25rGgG+KOy0r8clYwL1CFd+PGwRqqMFVQ4/Qd3sQu5t7ucP7C/Uw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", + "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", "cpu": [ "x64" ], @@ -4174,9 +4175,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.1.tgz", - "integrity": "sha512-uBkwaI+gBUlIe+EfbNnY5xNyXuhZbDSx2nzzW8tRMjUmpScd6lCQYKY2V9BATHtv5Ef2OBq6SChEP8h+/cxifQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", + "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", "cpu": [ "arm" ], @@ -4187,9 +4188,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.1.tgz", - "integrity": "sha512-0bK9aG1kIg0Su7OcFTlexkVeNZ5IzEsnz1ept87a0TUgZ6HplSgkJAnFpEVRW7GRcikT4GlPV0pbtVedOaXHQQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", + "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", "cpu": [ "arm64" ], @@ -4200,9 +4201,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.1.tgz", - "integrity": "sha512-qB6AFRXuP8bdkBI4D7UPUbE7OQf7u5OL+R94JE42Z2Qjmyj74FtDdLGeriRyBDhm4rQSvqAGCGC01b8Fu2LthQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", + "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", "cpu": [ "arm64" ], @@ -4213,9 +4214,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.1.tgz", - "integrity": "sha512-sHig3LaGlpNgDj5o8uPEoGs98RII8HpNIqFtAI8/pYABO8i0nb1QzT0JDoXF/pxzqO+FkxvwkHZo9k0NJYDedg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", + "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", "cpu": [ "riscv64" ], @@ -4226,9 +4227,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.1.tgz", - "integrity": "sha512-nD3YcUv6jBJbBNFvSbp0IV66+ba/1teuBcu+fBBPZ33sidxitc6ErhON3JNavaH8HlswhWMC3s5rgZpM4MtPqQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", + "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", "cpu": [ "x64" ], @@ -4239,9 +4240,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.1.tgz", - "integrity": "sha512-7/XVZqgBby2qp/cO0TQ8uJK+9xnSdJ9ct6gSDdEr4MfABrjTyrW6Bau7HQ73a2a5tPB7hno49A0y1jhWGDN9OQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", + "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", "cpu": [ "x64" ], @@ -4252,9 +4253,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.1.tgz", - "integrity": "sha512-CYc64bnICG42UPL7TrhIwsJW4QcKkIt9gGlj21gq3VV0LL6XNb1yAdHVp1pIi9gkts9gGcT3OfUYHjGP7ETAiw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", + "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", "cpu": [ "arm64" ], @@ -4265,9 +4266,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.1.tgz", - "integrity": "sha512-LN+vnlZ9g0qlHGlS920GR4zFCqAwbv2lULrR29yGaWP9u7wF5L7GqWu9Ah6/kFZPXPUkpdZwd//TNR+9XC9hvA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", + "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", "cpu": [ "ia32" ], @@ -4278,9 +4279,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.1.tgz", - "integrity": "sha512-n+vkrSyphvmU0qkQ6QBNXCGr2mKjhP08mPRM/Xp5Ck2FV4NrHU+y6axzDeixUrCBHVUS51TZhjqrKBBsHLKb2Q==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", "cpu": [ "x64" ], @@ -4576,9 +4577,9 @@ "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==" }, "node_modules/@types/lodash": { - "version": "4.14.202", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", "peer": true }, "node_modules/@types/lodash-es": { @@ -4635,9 +4636,9 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/node": { - "version": "20.11.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz", - "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==", + "version": "20.11.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz", + "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==", "dependencies": { "undici-types": "~5.26.4" } @@ -4757,16 +4758,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.1.tgz", - "integrity": "sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz", + "integrity": "sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/type-utils": "7.1.1", - "@typescript-eslint/utils": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/type-utils": "7.2.0", + "@typescript-eslint/utils": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -4825,15 +4826,15 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.1.tgz", - "integrity": "sha512-ZWUFyL0z04R1nAEgr9e79YtV5LbafdOtN7yapNbn1ansMyaegl2D4bL7vHoJ4HPSc4CaLwuCVas8CVuneKzplQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4" }, "engines": { @@ -4853,13 +4854,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz", - "integrity": "sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1" + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -4870,13 +4871,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.1.tgz", - "integrity": "sha512-5r4RKze6XHEEhlZnJtR3GYeCh1IueUHdbrukV2KSlLXaTjuSfeVF8mZUVPLovidCuZfbVjfhi4c0DNSa/Rdg5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz", + "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/utils": "7.1.1", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/utils": "7.2.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -4897,9 +4898,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.1.tgz", - "integrity": "sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -4910,13 +4911,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz", - "integrity": "sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -5009,17 +5010,17 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.1.tgz", - "integrity": "sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz", + "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", "semver": "^7.5.4" }, "engines": { @@ -5067,12 +5068,12 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz", - "integrity": "sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", + "@typescript-eslint/types": "7.2.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -5257,9 +5258,9 @@ } }, "node_modules/@vuepress/helper": { - "version": "2.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@vuepress/helper/-/helper-2.0.0-rc.18.tgz", - "integrity": "sha512-Nh4q32qrm9Dpji0WaWU9yjhpxQ4nZXG8kq8XVIiZt7PHM75Q/CoofJWGKOt8qIafBKXtDUClVXLO2Xxp4ae9zg==", + "version": "2.0.0-rc.19", + "resolved": "https://registry.npmjs.org/@vuepress/helper/-/helper-2.0.0-rc.19.tgz", + "integrity": "sha512-g8udvFCIBcEcpLTo1BFZw452oBmnflW3lCmN0rR+SfIkZymi9CnFV8LgxTF/KV7vB71QMjN8IAwCVvJ3pGCUag==", "dev": true, "dependencies": { "@vue/shared": "^3.4.21", @@ -5309,12 +5310,12 @@ } }, "node_modules/@vuepress/plugin-back-to-top": { - "version": "2.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-back-to-top/-/plugin-back-to-top-2.0.0-rc.18.tgz", - "integrity": "sha512-NMaBWfj3fh5mpC6IKpBb+jO3oludU3UNXLd+ix8QSAnkBLnrQwDXSVlfWSZwqdotrFYrxW5KFBGR/1nw/SZrbQ==", + "version": "2.0.0-rc.19", + "resolved": "https://registry.npmjs.org/@vuepress/plugin-back-to-top/-/plugin-back-to-top-2.0.0-rc.19.tgz", + "integrity": "sha512-biR4S/7r8zwdukASG6o4JWv5Lp3SqPnOJnCHSUtZPnRqJsdxrSfYR62zgXfD5xukD+9PwqmwOdI5M5K0aHyytw==", "dev": true, "dependencies": { - "@vuepress/helper": "~2.0.0-rc.18", + "@vuepress/helper": "~2.0.0-rc.19", "@vueuse/core": "^10.9.0", "vue": "^3.4.21" }, @@ -5337,12 +5338,12 @@ } }, "node_modules/@vuepress/plugin-copy-code": { - "version": "2.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-copy-code/-/plugin-copy-code-2.0.0-rc.18.tgz", - "integrity": "sha512-9gAhPVn2dyFnpIWZzHVQdE8iNXZQP2C0x2oBbU23IW4AG66TXETS0iB1WYnffqpq7dBlzO/6MbeiORtZqdHshA==", + "version": "2.0.0-rc.19", + "resolved": "https://registry.npmjs.org/@vuepress/plugin-copy-code/-/plugin-copy-code-2.0.0-rc.19.tgz", + "integrity": "sha512-V85jVkTk5kjZ6LaXbudqBxdRy8Mqc8k7EN+Os3RIhUMBdHwgDkmTmS1QM6eOlKK19Yaw7MHtPFYS7NR262XnPQ==", "dev": true, "dependencies": { - "@vuepress/helper": "~2.0.0-rc.18", + "@vuepress/helper": "~2.0.0-rc.19", "@vueuse/core": "^10.9.0", "vue": "^3.4.21" }, @@ -5375,24 +5376,24 @@ } }, "node_modules/@vuepress/plugin-links-check": { - "version": "2.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-links-check/-/plugin-links-check-2.0.0-rc.18.tgz", - "integrity": "sha512-TqAZNqyNUj2SnZ2Mo1P3ufCnJWBB9sv2YqZSFbgtYoQhhNo3zkwhflOxeC/jNVaH+rw4azdD0iMFOTU41imoHw==", + "version": "2.0.0-rc.19", + "resolved": "https://registry.npmjs.org/@vuepress/plugin-links-check/-/plugin-links-check-2.0.0-rc.19.tgz", + "integrity": "sha512-DGcQ+xAnPAHT0JWifTVxEaH+U14IsRcuW9vuqn9n03+3xot2OkabxDa2zk5XgAcSn3QNg/pkLkImqTNRJ780eA==", "dev": true, "dependencies": { - "@vuepress/helper": "~2.0.0-rc.18" + "@vuepress/helper": "~2.0.0-rc.19" }, "peerDependencies": { "vuepress": "2.0.0-rc.8" } }, "node_modules/@vuepress/plugin-medium-zoom": { - "version": "2.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-medium-zoom/-/plugin-medium-zoom-2.0.0-rc.18.tgz", - "integrity": "sha512-szO65QaKUk5S0UYtEIWngkI/vXV0B1INiwgiGKSYabL6bLkJe1Fyv+8VT3Hos+aqdh+J+35ud+cIMI0nxUAqKw==", + "version": "2.0.0-rc.19", + "resolved": "https://registry.npmjs.org/@vuepress/plugin-medium-zoom/-/plugin-medium-zoom-2.0.0-rc.19.tgz", + "integrity": "sha512-zoIdSscgPwR252NKld6v1VnOKqEXIxW9KIj9S64AkzFtZdg/8ckTmOzw8VJ1ufMO3NUlqWlz4dIjsl9ckVudVQ==", "dev": true, "dependencies": { - "@vuepress/helper": "~2.0.0-rc.18", + "@vuepress/helper": "~2.0.0-rc.19", "medium-zoom": "^1.1.0", "vue": "^3.4.21" }, @@ -5450,24 +5451,24 @@ } }, "node_modules/@vuepress/plugin-seo": { - "version": "2.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-seo/-/plugin-seo-2.0.0-rc.18.tgz", - "integrity": "sha512-wTJqXIn+edDnKlL0ZOf7MLDQo59fhLePfsrpsCbaD8BKzHmEkx5aT1FF27JOvsRmMH4muv1uFQRXP837BsYzzw==", + "version": "2.0.0-rc.19", + "resolved": "https://registry.npmjs.org/@vuepress/plugin-seo/-/plugin-seo-2.0.0-rc.19.tgz", + "integrity": "sha512-/lTL6dFkuCK16M5yDZdD7zohdy+OqqeUjY1ZsXM2bYGjaha5CiukuUhJlIfRmM9oFQEOBirWCKPC0Ns4ObhPLA==", "dev": true, "dependencies": { - "@vuepress/helper": "~2.0.0-rc.18" + "@vuepress/helper": "~2.0.0-rc.19" }, "peerDependencies": { "vuepress": "2.0.0-rc.8" } }, "node_modules/@vuepress/plugin-sitemap": { - "version": "2.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-sitemap/-/plugin-sitemap-2.0.0-rc.18.tgz", - "integrity": "sha512-xDPZJWRD2bZhBPR0VA8F8jum5c8DV5P0+mvaQX6Vnugxo+0kETZSM5ctETA79CTmDdKuG6IW0tJkYK7ysb49zw==", + "version": "2.0.0-rc.19", + "resolved": "https://registry.npmjs.org/@vuepress/plugin-sitemap/-/plugin-sitemap-2.0.0-rc.19.tgz", + "integrity": "sha512-sIND+03O8h222BljQaSZQ8g7y+bHPZyjhEW8c8cLXv0/LZ7apO1qoEuU11ILYHZw5BF/zKNvRriw9QvrEDlIxA==", "dev": true, "dependencies": { - "@vuepress/helper": "~2.0.0-rc.18", + "@vuepress/helper": "~2.0.0-rc.19", "sitemap": "^7.1.1" }, "peerDependencies": { @@ -5496,25 +5497,25 @@ } }, "node_modules/@vuepress/theme-default": { - "version": "2.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@vuepress/theme-default/-/theme-default-2.0.0-rc.18.tgz", - "integrity": "sha512-YcN4govU647we80OS/11W5cw+aliY5pXGbYJBRrIoIj/j10RKj6tDDAfv+orKt3lyswBRQQLAhP3NGDfsmx8+w==", + "version": "2.0.0-rc.20", + "resolved": "https://registry.npmjs.org/@vuepress/theme-default/-/theme-default-2.0.0-rc.20.tgz", + "integrity": "sha512-fXiUNMdmG2d1V6RRigm9/lMHKrJp1MoXPO4CRxdJnfpfxlzgTA9JqEPfgBqeovS1OE+pm+GrFSxRToAHSfS68g==", "dev": true, "dependencies": { - "@vuepress/helper": "~2.0.0-rc.18", + "@vuepress/helper": "~2.0.0-rc.19", "@vuepress/plugin-active-header-links": "~2.0.0-rc.18", - "@vuepress/plugin-back-to-top": "~2.0.0-rc.18", + "@vuepress/plugin-back-to-top": "~2.0.0-rc.19", "@vuepress/plugin-container": "~2.0.0-rc.15", - "@vuepress/plugin-copy-code": "~2.0.0-rc.18", + "@vuepress/plugin-copy-code": "~2.0.0-rc.19", "@vuepress/plugin-external-link-icon": "~2.0.0-rc.18", "@vuepress/plugin-git": "~2.0.0-rc.15", - "@vuepress/plugin-links-check": "~2.0.0-rc.18", - "@vuepress/plugin-medium-zoom": "~2.0.0-rc.18", + "@vuepress/plugin-links-check": "~2.0.0-rc.19", + "@vuepress/plugin-medium-zoom": "~2.0.0-rc.19", "@vuepress/plugin-nprogress": "~2.0.0-rc.18", "@vuepress/plugin-palette": "~2.0.0-rc.15", "@vuepress/plugin-prismjs": "~2.0.0-rc.15", - "@vuepress/plugin-seo": "~2.0.0-rc.18", - "@vuepress/plugin-sitemap": "~2.0.0-rc.18", + "@vuepress/plugin-seo": "~2.0.0-rc.19", + "@vuepress/plugin-sitemap": "~2.0.0-rc.19", "@vuepress/plugin-theme-data": "~2.0.0-rc.18", "@vueuse/core": "^10.9.0", "sass": "^1.71.1", @@ -6235,13 +6236,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", - "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.9.tgz", + "integrity": "sha512-BXIWIaO3MewbXWdJdIGDWZurv5OGJlFNo7oy20DpB3kWDVJLcY2NRypRsRUbRe5KMqSNLuOGnWTFQQtY5MAsRw==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.5.0", + "@babel/helper-define-polyfill-provider": "^0.6.0", "semver": "^6.3.1" }, "peerDependencies": { @@ -6261,6 +6262,22 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/babel-plugin-polyfill-regenerator": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", @@ -6273,6 +6290,22 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-plugin-polyfill-regenerator/node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -6712,9 +6745,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001596", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001596.tgz", - "integrity": "sha512-zpkZ+kEr6We7w63ORkoJ2pOfBwBkY/bJrG/UZ90qNb45Isblu8wzDgevEOrRL1r9dWayHjYiiyCMEXPn4DweGQ==", + "version": "1.0.30001597", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", + "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", "funding": [ { "type": "opencollective", @@ -8397,9 +8430,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.698", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.698.tgz", - "integrity": "sha512-f9iZD1t3CLy1AS6vzM5EKGa6p9pRcOeEFXRFbaG2Ta+Oe7MkfRQ3fsvPYidzHe1h4i0JvIvpcY55C+B6BZNGtQ==" + "version": "1.4.701", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz", + "integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -8515,9 +8548,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.15.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.1.tgz", - "integrity": "sha512-3d3JRbwsCLJsYgvb6NuWEG44jjPSOMuS73L/6+7BZuoKm3W+qXnSoIYVHi8dG7Qcg4inAY4jbzkZ7MnskePeDg==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", + "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -10025,9 +10058,9 @@ "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==" }, "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -17506,9 +17539,9 @@ } }, "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.11.1.tgz", - "integrity": "sha512-MFMf6VkEVZAETidGGSYW2B1MjXbGX+sWIywn2QPEaJ3j08V+MwVRHMXtf2noB8ENJaD0LIun9wh5Z6OPNf1QzQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.12.0.tgz", + "integrity": "sha512-5Y2/pp2wtJk8o08G0CMkuFPCO354FGwk/vbidxrdhRGZfd0tFnb4Qb8anp9XxXriwBgVPjdWbKpGl4J9lJY2jQ==", "dev": true, "engines": { "node": ">=16" @@ -17561,9 +17594,9 @@ } }, "node_modules/read-pkg/node_modules/type-fest": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.11.1.tgz", - "integrity": "sha512-MFMf6VkEVZAETidGGSYW2B1MjXbGX+sWIywn2QPEaJ3j08V+MwVRHMXtf2noB8ENJaD0LIun9wh5Z6OPNf1QzQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.12.0.tgz", + "integrity": "sha512-5Y2/pp2wtJk8o08G0CMkuFPCO354FGwk/vbidxrdhRGZfd0tFnb4Qb8anp9XxXriwBgVPjdWbKpGl4J9lJY2jQ==", "dev": true, "engines": { "node": ">=16" @@ -17931,9 +17964,9 @@ } }, "node_modules/rollup": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.1.tgz", - "integrity": "sha512-ggqQKvx/PsB0FaWXhIvVkSWh7a/PCLQAsMjBc+nA2M8Rv2/HG0X6zvixAB7KyZBRtifBUhy5k8voQX/mRnABPg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", + "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -17946,19 +17979,19 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.12.1", - "@rollup/rollup-android-arm64": "4.12.1", - "@rollup/rollup-darwin-arm64": "4.12.1", - "@rollup/rollup-darwin-x64": "4.12.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.12.1", - "@rollup/rollup-linux-arm64-gnu": "4.12.1", - "@rollup/rollup-linux-arm64-musl": "4.12.1", - "@rollup/rollup-linux-riscv64-gnu": "4.12.1", - "@rollup/rollup-linux-x64-gnu": "4.12.1", - "@rollup/rollup-linux-x64-musl": "4.12.1", - "@rollup/rollup-win32-arm64-msvc": "4.12.1", - "@rollup/rollup-win32-ia32-msvc": "4.12.1", - "@rollup/rollup-win32-x64-msvc": "4.12.1", + "@rollup/rollup-android-arm-eabi": "4.13.0", + "@rollup/rollup-android-arm64": "4.13.0", + "@rollup/rollup-darwin-arm64": "4.13.0", + "@rollup/rollup-darwin-x64": "4.13.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", + "@rollup/rollup-linux-arm64-gnu": "4.13.0", + "@rollup/rollup-linux-arm64-musl": "4.13.0", + "@rollup/rollup-linux-riscv64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-musl": "4.13.0", + "@rollup/rollup-win32-arm64-msvc": "4.13.0", + "@rollup/rollup-win32-ia32-msvc": "4.13.0", + "@rollup/rollup-win32-x64-msvc": "4.13.0", "fsevents": "~2.3.2" } }, @@ -18592,16 +18625,16 @@ "dev": true }, "node_modules/set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.2", + "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -19512,9 +19545,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", - "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "engines": { "node": ">=16" @@ -20354,9 +20387,9 @@ } }, "node_modules/webpack-dev-server/node_modules/open": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/open/-/open-10.0.4.tgz", - "integrity": "sha512-oujJ/FFr7ra6/7gJuQ4ZJJ8Gf2VHM0J3J/W7IvH++zaqEzacWVxzK++NiVY5NLHTTj7u/jNH5H3Ei9biL31Lng==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", @@ -20706,9 +20739,9 @@ } }, "node_modules/write-pkg/node_modules/type-fest": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.11.1.tgz", - "integrity": "sha512-MFMf6VkEVZAETidGGSYW2B1MjXbGX+sWIywn2QPEaJ3j08V+MwVRHMXtf2noB8ENJaD0LIun9wh5Z6OPNf1QzQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.12.0.tgz", + "integrity": "sha512-5Y2/pp2wtJk8o08G0CMkuFPCO354FGwk/vbidxrdhRGZfd0tFnb4Qb8anp9XxXriwBgVPjdWbKpGl4J9lJY2jQ==", "dev": true, "engines": { "node": ">=16" @@ -20810,22 +20843,20 @@ } }, "packages/contracts": { - "name": "@aedart/contracts", "version": "0.10.0", "license": "BSD-3-Clause" }, "packages/support": { - "name": "@aedart/support", "version": "0.10.0", "license": "BSD-3-Clause", "peerDependencies": { "@aedart/contracts": "^0.10.0", "@types/lodash-es": "^4.17.12", - "lodash-es": "^4.17.21" + "lodash-es": "^4.17.21", + "tslib": "^2.6.2" } }, "packages/vuepress-utils": { - "name": "@aedart/vuepress-utils", "version": "0.10.0", "license": "BSD-3-Clause", "peerDependencies": { @@ -20839,7 +20870,6 @@ } }, "packages/xyz": { - "name": "@aedart/xyz", "version": "0.10.0", "license": "BSD-3-Clause", "peerDependencies": { diff --git a/package.json b/package.json index 2e5d9a7a..6fedd104 100644 --- a/package.json +++ b/package.json @@ -23,13 +23,14 @@ "node": "^20.11.0" }, "devDependencies": { - "@babel/cli": "^7.23.4", - "@babel/core": "^7.23.7", + "@babel/cli": "^7.23.9", + "@babel/core": "^7.24.0", "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-decorators": "^7.23.7", "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-transform-class-static-block": "^7.23.4", "@babel/preset-env": "^7.23.8", + "@babel/runtime": "^7.24.0", "@lerna-lite/changed": "^3.2.1", "@lerna-lite/cli": "^3.2.1", "@lerna-lite/list": "^3.2.1", From d7954f24010893a7d25370dd205ab3d159a806d5 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 14:03:58 +0100 Subject: [PATCH 082/224] Change release notes --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae76f71f..3bf89f6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `ClassMethodName` and `ClassMethodReference` type aliases in `@aedart/contracts`. * `isMethod()` util in `@aedart/support/reflections`. * `ConstructorLike` and `Callback` type aliases, in `@aedart/constracts`. -* `CallbackWrapper` interface, in `@aedart/constracts/support`. * `CallbackWrapper` util class, in `@aedart/support`. * `isCallbackWrapper` util, in `@aedart/support`. +* `ArbitraryData` concern, in `@aedart/support`. * Add upgrade guide from v0.7.x- to v0.10.x. ### Changed @@ -30,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Root package Typescript dependency changed to `^5.4.2`. * `@typescript-eslint/eslint-plugin` upgraded to `^7.1.1`, in root package. -* Decorator return types for `meta()`, `targetMeta()`, and `inheritTargetMeta()` (_continued to cause TS1270 and TS1238 errors_). [#8](https://github.com/aedart/ion/pull/8), [#9](https://github.com/aedart/ion/pull/9). +* Removed decorator return types for `use()`, `meta()`, `targetMeta()`, and `inheritTargetMeta()` (_continued to cause TS1270 and TS1238 errors_). [#8](https://github.com/aedart/ion/pull/8), [#9](https://github.com/aedart/ion/pull/9). * Refactored `hasAllMethods()` to use new `isMethod()` internally, in `@aedart/support/reflections`. * Refactored all components that used deprecated `ConstructorOrAbstractConstructor` to use new `ConstructorLike` type alias. * Marked `isClassConstructor()` and `isCallable()` as stable, in `@aedart/support/reflections`. @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Decorator types aliases (_TS1270 and TS1238 issues when applying the various decorator and decorator result types_). [#8](https://github.com/aedart/ion/pull/8). * Broken link in docs for `isArrayLike`. +* Missing `tslib` as peer dependency for `@aedart/support` package. ### Deprecated From d9940a228086aa5747be8fb6431af1a19e8e7869 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 14:11:20 +0100 Subject: [PATCH 083/224] Add JSDoc for call() --- packages/contracts/src/container/Container.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index c1b281b2..25ba1991 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -14,7 +14,7 @@ import Binding from "./Binding"; /** * Service Container * - * Inspired by Psr's `ContainerInterface`, and Laravel's service `Container`. + * Adaptation of Psr's `ContainerInterface`, and Laravel's service `Container` * * @see https://www.php-fig.org/psr/psr-11/#31-psrcontainercontainerinterface * @see https://github.com/laravel/framework/blob/master/src/Illuminate/Contracts/Container/Container.php @@ -179,8 +179,17 @@ export default interface Container * @throws {ContainerException} */ build(concrete: Constructor | Binding): T; - - // TODO: ... + + /** + * Call given method and inject dependencies if needed + * + * @param {Callback | CallbackWrapper | ClassMethodReference} method + * @param {any[]} args + * + * @return {any} + * + * @throws {ContainerException} + */ call(method: Callback | CallbackWrapper | ClassMethodReference, args: any[]): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ /** From c5db4d39d5561bff8e92149c5933d8cd4bb5736b Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 14:23:00 +0100 Subject: [PATCH 084/224] Add ExtendCallback type alias --- packages/contracts/src/container/types.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/container/types.ts b/packages/contracts/src/container/types.ts index f7514bbe..3288891f 100644 --- a/packages/contracts/src/container/types.ts +++ b/packages/contracts/src/container/types.ts @@ -23,4 +23,15 @@ export type Alias = Identifier; */ export type FactoryCallback< Value = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ -> = (container: Container, ...args: any[]) => Value; \ No newline at end of file +> = (container: Container, ...args: any[]) => Value; + +/** + * Extend Callback + * + * Callback can be used to "extend", decorate or modify a resolved value + * from the service container. + */ +export type ExtendCallback< + Value = any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ExtendedValue extends Value = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +> = (resolved: Value, container: Container) => ExtendedValue; \ No newline at end of file From 2e3dd0def4fe870efe820b19a64c5f9d25487576 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 14:23:12 +0100 Subject: [PATCH 085/224] Change Container, add extend() method --- packages/contracts/src/container/Container.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index 25ba1991..2886a1e3 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -7,7 +7,8 @@ import { CallbackWrapper } from "@aedart/contracts/support"; import { Alias, Identifier, - FactoryCallback + FactoryCallback, + ExtendCallback, } from "./types"; import Binding from "./Binding"; @@ -191,6 +192,19 @@ export default interface Container * @throws {ContainerException} */ call(method: Callback | CallbackWrapper | ClassMethodReference, args: any[]): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + /** + * Extend the registered binding + * + * @param {Identifier} identifier + * @param {ExtendCallback} callback + * + * @return {this} + * + * @throws {TypeError} + * @throws {ContainerException} + */ + extend(identifier: Identifier, callback: ExtendCallback): this; /** * Forget binding and resolved instance for given identifier From f28de5333148b3109750ebec9c51f9274caf77d7 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 14:53:17 +0100 Subject: [PATCH 086/224] Add before and after resolving hook callbacks --- packages/contracts/src/container/Container.ts | 33 ++++++++++++++++++- packages/contracts/src/container/types.ts | 18 +++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index 2886a1e3..b3554423 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -9,13 +9,15 @@ import { Identifier, FactoryCallback, ExtendCallback, + BeforeResolvedCallback, + AfterResolvedCallback, } from "./types"; import Binding from "./Binding"; /** * Service Container * - * Adaptation of Psr's `ContainerInterface`, and Laravel's service `Container` + * Adaptation of Psr's `ContainerInterface`, and Laravel's service `Container`. * * @see https://www.php-fig.org/psr/psr-11/#31-psrcontainercontainerinterface * @see https://github.com/laravel/framework/blob/master/src/Illuminate/Contracts/Container/Container.php @@ -221,4 +223,33 @@ export default interface Container * @returns {void} */ flush(): void; + + /** + * Determine if identifier has been resolved + * + * @param {Identifier} identifier + * + * @return {boolean} + */ + isResolved(identifier: Identifier): boolean; + + /** + * Register a callback to be invoked before a binding is resolved + * + * @param {Identifier} identifier + * @param {BeforeResolvedCallback} callback + * + * @return {this} + */ + before(identifier: Identifier, callback: BeforeResolvedCallback): this; + + /** + * Register a callback to be invoked after a binding has been resolved + * + * @param {Identifier} identifier + * @param {AfterResolvedCallback} callback + * + * @return {this} + */ + after(identifier: Identifier, callback: AfterResolvedCallback): this; } \ No newline at end of file diff --git a/packages/contracts/src/container/types.ts b/packages/contracts/src/container/types.ts index 3288891f..3ecded44 100644 --- a/packages/contracts/src/container/types.ts +++ b/packages/contracts/src/container/types.ts @@ -34,4 +34,20 @@ export type FactoryCallback< export type ExtendCallback< Value = any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ ExtendedValue extends Value = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ -> = (resolved: Value, container: Container) => ExtendedValue; \ No newline at end of file +> = (resolved: Value, container: Container) => ExtendedValue; + +/** + * Before Resolved Callback + * + * Callback to be invoked before a binding is resolved. + */ +export type BeforeResolvedCallback = (identifier: Identifier, args: any[], container: Container) => void; + +/** + * After Resolved Callback + * + * Callback to be invoked after a binding has been resolved. + */ +export type AfterResolvedCallback< + Value = any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ +> = (identifier: Identifier, resolved: Value, container: Container) => void; \ No newline at end of file From 40977e88318316bb9030c33c946768863406be18 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 12 Mar 2024 14:56:04 +0100 Subject: [PATCH 087/224] Fix style --- packages/contracts/src/container/types.ts | 12 ++++++++++-- packages/support/src/ArbitraryData.ts | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/contracts/src/container/types.ts b/packages/contracts/src/container/types.ts index 3ecded44..ea9b1285 100644 --- a/packages/contracts/src/container/types.ts +++ b/packages/contracts/src/container/types.ts @@ -41,7 +41,11 @@ export type ExtendCallback< * * Callback to be invoked before a binding is resolved. */ -export type BeforeResolvedCallback = (identifier: Identifier, args: any[], container: Container) => void; +export type BeforeResolvedCallback = ( + identifier: Identifier, + args: any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + container: Container +) => void; /** * After Resolved Callback @@ -50,4 +54,8 @@ export type BeforeResolvedCallback = (identifier: Identifier, args: any[], conta */ export type AfterResolvedCallback< Value = any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ -> = (identifier: Identifier, resolved: Value, container: Container) => void; \ No newline at end of file +> = ( + identifier: Identifier, + resolved: Value, + container: Container +) => void; \ No newline at end of file diff --git a/packages/support/src/ArbitraryData.ts b/packages/support/src/ArbitraryData.ts index 5a24ea12..812c337d 100644 --- a/packages/support/src/ArbitraryData.ts +++ b/packages/support/src/ArbitraryData.ts @@ -19,7 +19,7 @@ export default class ArbitraryData extends AbstractConcern implements HasArbitra * * @private */ - #data: Record = {}; + #data: Record = {}; /* eslint-disable-line @typescript-eslint/no-explicit-any */ /** * Set value for key From 642e6bd94f9bff47d5c1a028f7a7fc9ed7801f73 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 10:57:20 +0100 Subject: [PATCH 088/224] Deprecate service container symbol This does not add any value. --- packages/container/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/container/src/index.ts b/packages/container/src/index.ts index 02b19635..f35197ee 100644 --- a/packages/container/src/index.ts +++ b/packages/container/src/index.ts @@ -1,4 +1,6 @@ /** + * @deprecated - To be removed - adds no value! + * * Service Container identifier * * @type {Symbol} From d2257fe5941767b0a5f20ee302fa6be1bb0db943 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 11:21:31 +0100 Subject: [PATCH 089/224] Fix "not under root dir" TypeScript warning --- packages/container/tsconfig.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/container/tsconfig.json b/packages/container/tsconfig.json index 27be036a..40b599ac 100644 --- a/packages/container/tsconfig.json +++ b/packages/container/tsconfig.json @@ -8,5 +8,7 @@ "include": [ "./src/**/*" ], - "references": [] + "references": [ + { "path": "../contracts", "prepend": false } + ] } From a0b7ff23e93a0fc30052679a5a020fcf4ccbc550 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 11:39:55 +0100 Subject: [PATCH 090/224] Fix "not under root dir" TypeScript waning for "support" package --- packages/container/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/container/tsconfig.json b/packages/container/tsconfig.json index 40b599ac..463dd768 100644 --- a/packages/container/tsconfig.json +++ b/packages/container/tsconfig.json @@ -9,6 +9,7 @@ "./src/**/*" ], "references": [ - { "path": "../contracts", "prepend": false } + { "path": "../contracts", "prepend": false }, + { "path": "../support", "prepend": false }, ] } From dbd844566ef83055983693ea80d28ed0a0cf1fe8 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 11:40:11 +0100 Subject: [PATCH 091/224] Add Binding Entry class --- packages/container/src/BindingEntry.ts | 151 +++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 packages/container/src/BindingEntry.ts diff --git a/packages/container/src/BindingEntry.ts b/packages/container/src/BindingEntry.ts new file mode 100644 index 00000000..1d299307 --- /dev/null +++ b/packages/container/src/BindingEntry.ts @@ -0,0 +1,151 @@ +import type { + Binding, + FactoryCallback, + Identifier +} from "@aedart/contracts/container"; +import type { Constructor } from "@aedart/contracts"; +import { isConstructor } from "@aedart/support/reflections"; + +/** + * Binding Entry + */ +export default class BindingEntry implements Binding +{ + /** + * This binding's identifier + * + * @type {Identifier} + * + * @readonly + */ + readonly #identifier: Identifier; + + /** + * The bound value to be resolved by a service container + * + * @template T = any + * + * @type {FactoryCallback | Constructor} + * + * @readonly + */ + readonly #value: FactoryCallback | Constructor; + + /** + * Shared state of resolved value + * + * @type {boolean} If `true`, then service container must register resolved + * value as a singleton. + * + * @readonly + */ + readonly #shared: boolean; + + /** + * State, whether value is a factory callback or not + * + * @type {boolean|null} + * + * @private + */ + #isFactoryCallback: boolean|null = null; + + /** + * State, whether value is a constructor or not + * + * @type {boolean|null} + * + * @private + */ + #isConstructor: boolean|null = null; + + /** + * Create new Binding Entry instance + * + * @template T = any + * + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} value + * @param {boolean} [shared=false] + */ + constructor(identifier: Identifier, value: FactoryCallback | Constructor, shared: boolean = false) + { + this.#identifier = identifier; + this.#value = value; + this.#shared = shared; + + this.resolveIsConstructorOrFactoryCallback(); + } + + /** + * This binding's identifier + * + * @type {Identifier} + * + * @readonly + */ + get identifier(): Identifier + { + return this.#identifier; + } + + /** + * The bound value to be resolved by a service container + * + * @template T = any + * + * @type {FactoryCallback | Constructor} + * + * @readonly + */ + get value(): FactoryCallback | Constructor + { + return this.#value; + } + + /** + * Shared state of resolved value + * + * @type {boolean} If `true`, then service container must register resolved + * value as a singleton. + * + * @readonly + */ + get shared(): boolean + { + return this.#shared; + } + + /** + * Determine if bound value is a {@link FactoryCallback} + * + * @returns {boolean} + */ + isFactoryCallback(): boolean + { + return this.#isFactoryCallback as boolean; + } + + /** + * Determine if bound value is a {@link Constructor} + * + * @returns {boolean} + */ + isConstructor(): boolean + { + return this.#isConstructor as boolean; + } + + /** + * Resolves the "is constructor" or "is factory callback" values + * + * @return {void} + * + * @protected + */ + protected resolveIsConstructorOrFactoryCallback(): void + { + this.#isConstructor = isConstructor(this.#value); + this.#isFactoryCallback = !this.#isConstructor; + } +} \ No newline at end of file From 30284100de6c909e02c3770e61623539ddf027d2 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 11:40:29 +0100 Subject: [PATCH 092/224] Export Binding Entry Also, removed the deprecated service container symbol. --- packages/container/src/index.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/container/src/index.ts b/packages/container/src/index.ts index f35197ee..1d72b54d 100644 --- a/packages/container/src/index.ts +++ b/packages/container/src/index.ts @@ -1,8 +1,4 @@ -/** - * @deprecated - To be removed - adds no value! - * - * Service Container identifier - * - * @type {Symbol} - */ -export const SERVICE_CONTAINER: unique symbol = Symbol('@aedart/container'); \ No newline at end of file +import BindingEntry from "./BindingEntry"; +export { + BindingEntry +} \ No newline at end of file From 19b87cb7b45cd8bdb15812a9435cb738d5f56ac8 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 11:44:28 +0100 Subject: [PATCH 093/224] Mark support packages as external --- packages/container/rollup.config.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/container/rollup.config.mjs b/packages/container/rollup.config.mjs index c58efd45..fb24e061 100644 --- a/packages/container/rollup.config.mjs +++ b/packages/container/rollup.config.mjs @@ -11,5 +11,12 @@ export default createConfig({ '@aedart/contracts/support/meta', '@aedart/contracts/support/mixins', '@aedart/contracts/support/reflections', + '@aedart/support', + '@aedart/support/arrays', + '@aedart/support/concerns', + '@aedart/support/exceptions', + '@aedart/support/meta', + '@aedart/support/mixins', + '@aedart/support/reflections', ] }); From dcea404bc69225171c00d9dad2a0a72928831b57 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 11:48:37 +0100 Subject: [PATCH 094/224] Throw TypeError in case of invalid binding value --- packages/container/src/BindingEntry.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/container/src/BindingEntry.ts b/packages/container/src/BindingEntry.ts index 1d299307..b3a2c927 100644 --- a/packages/container/src/BindingEntry.ts +++ b/packages/container/src/BindingEntry.ts @@ -146,6 +146,10 @@ export default class BindingEntry implements Binding protected resolveIsConstructorOrFactoryCallback(): void { this.#isConstructor = isConstructor(this.#value); - this.#isFactoryCallback = !this.#isConstructor; + this.#isFactoryCallback = !this.#isConstructor; + + if (this.#isConstructor === false && this.#isFactoryCallback === false) { + throw new TypeError('Binding value must either be a valid constructor or factory callback', { cause: { identifier: this.#identifier, value: this.#value } }); + } } } \ No newline at end of file From 4537a68991cd501081ea1fcb18ff294e59c980ed Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 13:12:33 +0100 Subject: [PATCH 095/224] Expand binding identifier to allow constructors and functions --- packages/contracts/src/container/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/container/types.ts b/packages/contracts/src/container/types.ts index ea9b1285..c9df040a 100644 --- a/packages/contracts/src/container/types.ts +++ b/packages/contracts/src/container/types.ts @@ -1,3 +1,4 @@ +import { Callback, ConstructorLike } from "@aedart/contracts"; import Container from "./Container"; /** @@ -6,7 +7,7 @@ import Container from "./Container"; * A unique identifier used for associating "concrete" items or values in * a service container. */ -export type Identifier = string | symbol | number | NonNullable; +export type Identifier = string | symbol | number | NonNullable | ConstructorLike | Callback; /** * Binding Alias From d5c7876f2d68b9a1273644ad653ccd4e8c84a74f Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 13:13:42 +0100 Subject: [PATCH 096/224] Add isBindingIdentifier() util --- packages/support/src/container/index.ts | 3 +- .../src/container/isBindingIdentifier.ts | 15 ++++++++ .../container/isBindingIdentifier.test.js | 35 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 packages/support/src/container/isBindingIdentifier.ts create mode 100644 tests/browser/packages/support/container/isBindingIdentifier.test.js diff --git a/packages/support/src/container/index.ts b/packages/support/src/container/index.ts index 3411a627..33ff2276 100644 --- a/packages/support/src/container/index.ts +++ b/packages/support/src/container/index.ts @@ -1,4 +1,5 @@ export * from "./dependencies"; export * from "./dependsOn"; export * from "./getDependencies"; -export * from "./hasDependencies"; \ No newline at end of file +export * from "./hasDependencies"; +export * from "./isBindingIdentifier"; \ No newline at end of file diff --git a/packages/support/src/container/isBindingIdentifier.ts b/packages/support/src/container/isBindingIdentifier.ts new file mode 100644 index 00000000..de6b0d90 --- /dev/null +++ b/packages/support/src/container/isBindingIdentifier.ts @@ -0,0 +1,15 @@ +/** + * Determine if value is of the type [Identifier]{@link import('@aedart/contracts/container').Identifier}. + * + * @param {unknown} value + * + * @return {boolean} + */ +export function isBindingIdentifier(value: unknown): boolean +{ + if (value === undefined || value === null) { + return false; + } + + return [ 'string', 'number', 'symbol', 'object', 'function' ].includes(typeof value); +} \ No newline at end of file diff --git a/tests/browser/packages/support/container/isBindingIdentifier.test.js b/tests/browser/packages/support/container/isBindingIdentifier.test.js new file mode 100644 index 00000000..b1f46b39 --- /dev/null +++ b/tests/browser/packages/support/container/isBindingIdentifier.test.js @@ -0,0 +1,35 @@ +import { isBindingIdentifier } from "@aedart/support/container"; + +describe('@aedart/support/container', () => { + describe('isBindingIdentifier', () => { + + it('can determine if value is a binding identifier', () => { + + class A {} + + const data = [ + { value: undefined, expected: false, name: 'undefined' }, + { value: null, expected: false, name: 'null' }, + + { value: 'lorum lipsum', expected: true, name: 'string' }, + { value: Symbol('my_identifier'), expected: true, name: 'symbol' }, + { value: 123, expected: true, name: 'number' }, + { value: {}, expected: true, name: 'object (empty)' }, + { value: [], expected: true, name: 'array (empty)' }, + { value: A, expected: true, name: 'class' }, + { value: class {}, expected: true, name: 'class (anonymous)' }, + { value: function() {}, expected: true, name: 'function' }, + { value: () => {}, expected: true, name: 'function (arrow)' }, + ]; + + data.forEach((entry, index) => { + + let result = isBindingIdentifier(entry.value); + expect(result) + .withContext(`${entry.name} was expected to be ${entry.expected}`) + .toBe(entry.expected); + }); + }); + + }); +}); \ No newline at end of file From 93f7326ce4c645dc355cdf40aeacb07401e9eada Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 13:13:47 +0100 Subject: [PATCH 097/224] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bf89f6b..4b9bea01 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 * `DEPENDENCIES` symbol and `Identifier` type in `@aedart/contracts/container`. * `dependsOn()`, `dependencies()`, `hasDependencies()`, and `getDependencies()`, in `@aedart/support/container`. +* `isBindingIdentifier`, in `@aedart/support/container`. * `ClassMethodName` and `ClassMethodReference` type aliases in `@aedart/contracts`. * `isMethod()` util in `@aedart/support/reflections`. * `ConstructorLike` and `Callback` type aliases, in `@aedart/constracts`. From 7468aca842935e08f3bf67c1878d228e07d8d2b5 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 13:24:02 +0100 Subject: [PATCH 098/224] Change Binding Entry - throw TypeError when identifier is not supported --- packages/container/src/BindingEntry.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/container/src/BindingEntry.ts b/packages/container/src/BindingEntry.ts index b3a2c927..7647fbb1 100644 --- a/packages/container/src/BindingEntry.ts +++ b/packages/container/src/BindingEntry.ts @@ -5,6 +5,7 @@ import type { } from "@aedart/contracts/container"; import type { Constructor } from "@aedart/contracts"; import { isConstructor } from "@aedart/support/reflections"; +import { isBindingIdentifier } from "@aedart/support/container"; /** * Binding Entry @@ -67,9 +68,15 @@ export default class BindingEntry implements Binding * @param {Identifier} identifier * @param {FactoryCallback | Constructor} value * @param {boolean} [shared=false] + * + * @throws {TypeError} */ constructor(identifier: Identifier, value: FactoryCallback | Constructor, shared: boolean = false) { + if (!isBindingIdentifier(identifier)) { + throw new TypeError(`Invalid binding identifier: ${typeof identifier} is not supported`, { cause: { identifier: identifier, value: value } }); + } + this.#identifier = identifier; this.#value = value; this.#shared = shared; From c43509757a8d541898301944c821b4e9b9701e75 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 13:42:44 +0100 Subject: [PATCH 099/224] Add tests for binding entry --- .../packages/container/BindingEntry.test.js | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tests/browser/packages/container/BindingEntry.test.js diff --git a/tests/browser/packages/container/BindingEntry.test.js b/tests/browser/packages/container/BindingEntry.test.js new file mode 100644 index 00000000..23d93d18 --- /dev/null +++ b/tests/browser/packages/container/BindingEntry.test.js @@ -0,0 +1,73 @@ +import { BindingEntry } from "@aedart/container"; + +describe('@aedart/container', () => { + describe('BindingEntry', () => { + + it('fails when binding identifier is invalid', () => { + + const callback = () => { + return new BindingEntry(null, () => false); + } + + expect(callback) + .toThrowError(TypeError); + }); + + it('fails when binding value is invalid', () => { + + const callback = () => { + return new BindingEntry(null, false); + } + + expect(callback) + .toThrowError(TypeError); + }); + + it('can create new binding entry', () => { + const identifier = 'a'; + class A {} + + const binding = new BindingEntry(identifier, A, true); + + expect(binding.identifier) + .withContext('Invalid identifier') + .toBe(identifier); + + expect(binding.value) + .withContext('Invalid value') + .toBe(A); + + expect(binding.shared) + .withContext('Invalid shared') + .toBeTrue(); + }); + + it('can determine if value is a constructor', () => { + + class A {} + + const binding = new BindingEntry('a', A); + + expect(binding.isConstructor()) + .withContext('Binding value should be a constructor') + .toBeTrue(); + + expect(binding.isFactoryCallback()) + .withContext('Binding value SHOULD NOT be a callback factory') + .toBeFalse(); + }); + + it('can determine if value is a constructor', () => { + + const binding = new BindingEntry('a', () => false); + + expect(binding.isConstructor()) + .withContext('Binding value SHOULD NOT be a constructor') + .toBeFalse(); + + expect(binding.isFactoryCallback()) + .withContext('Binding value should be a callback factory') + .toBeTrue(); + }); + }); +}); \ No newline at end of file From adf5784c4a602c8e3bdb2b99fdc3b17e78b53326 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 14:29:30 +0100 Subject: [PATCH 100/224] Add notice about Laravel Service Container --- packages/container/NOTICE | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/container/NOTICE b/packages/container/NOTICE index d638e176..9d6c6e20 100644 --- a/packages/container/NOTICE +++ b/packages/container/NOTICE @@ -11,3 +11,34 @@ conditions of the included 3rd party software, in the directory where it has bee "node_modules" directory. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@aedart/container (Service Container) + The service container is an adaptation of Laravel's Container + + See https://github.com/laravel/framework/blob/11.x/src/Illuminate/Container/Container.php + + License MIT, Copyright (c) Taylor Otwell. + + This part of the NOTICE file corresponds to terms and conditions set by the MIT License + ======================================================================================= + + The MIT License (MIT) + + Copyright (c) Taylor Otwell + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. \ No newline at end of file From b7a62a596a00beff4fa8fa6fd218200d27d6563d Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 14:34:18 +0100 Subject: [PATCH 101/224] Add service container class (incomplete) --- packages/container/src/Container.ts | 326 ++++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 packages/container/src/Container.ts diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts new file mode 100644 index 00000000..8ab8dfc5 --- /dev/null +++ b/packages/container/src/Container.ts @@ -0,0 +1,326 @@ +import type { + AfterResolvedCallback, + Alias, BeforeResolvedCallback, + Container as ServiceContainerContract, + ExtendCallback, + FactoryCallback, + Identifier, + Binding +} from "@aedart/contracts/container"; +import type { Callback, ClassMethodReference, Constructor } from "@aedart/contracts"; +import type { CallbackWrapper } from "@aedart/contracts/support"; + +/** + * Service Container + * + * Adaptation of Laravel's Service Container. + * + * @see https://github.com/laravel/framework/blob/11.x/src/Illuminate/Container/Container.php + */ +export default class Container implements ServiceContainerContract +{ + /** + * Register a binding + * + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} [concrete] + * @param {boolean} [shared=false] + * + * @returns {this} + * + * @throws {TypeError} + */ + bind(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared?: boolean): this + { + // TODO: Implement this... + return this; + } + + /** + * Register a binding, if none already exists for given identifier + * + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} [concrete] + * @param {boolean} [shared=false] + * + * @returns {this} + * + * @throws {TypeError} + */ + bindIf(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared?: boolean): this + { + // TODO: Implement this... + return this; + } + + /** + * Register a shared binding + * + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} [concrete] + * + * @returns {this} + * + * @throws {TypeError} + */ + singleton(identifier: Identifier, concrete?: FactoryCallback | Constructor): this + { + // TODO: Implement this... + return this; + } + + /** + * Register a shared binding, if none already exists for given identifier + * + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} [concrete] + * + * @returns {this} + * + * @throws {TypeError} + */ + singletonIf(identifier: Identifier, concrete?: FactoryCallback | Constructor): this + { + // TODO: Implement this... + return this; + } + + /** + * Register existing object instance as a shared binding + * + * @template T = object + * + * @param {Identifier} identifier + * @param {T} instance + * + * @returns {T} + * + * @throws {TypeError} + */ + instance(identifier: Identifier, instance: T): T + { + // TODO: Implement this... + return null as T; + } + + /** + * Resolves binding value that matches identifier and returns it + * + * @template T = any + * + * @param {Identifier} identifier + * + * @returns {T} + * + * @throws {NotFoundException} + * @throws {ContainerException} + */ + get< + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(identifier: Identifier): T + { + // TODO: Implement this... + return null as T; + } + + /** + * Determine if an entry is registered for given identifier. + * + * @param {Identifier} identifier + * + * @returns {boolean} + */ + has(identifier: Identifier): boolean + { + // TODO: Implement this... + return false; + } + + /** + * Alias for {@link has} + * + * @param {Identifier} identifier + * + * @returns {boolean} + */ + bound(identifier: Identifier): boolean + { + // TODO: Implement this... + return false; + } + + /** + * Alias identifier as a different identifier + * + * @param {Identifier} identifier + * @param {Alias} alias + * + * @returns {this} + * + * @throws {TypeError} + */ + alias(identifier: Identifier, alias: Alias): this + { + // TODO: Implement this... + return this; + } + + /** + * Resolves binding value that matches identifier and returns it + * + * @template T = any + * + * @param {Identifier} identifier + * @param {any[]} [args] Eventual arguments to pass on to {@link FactoryCallback} or {@link Constructor} + * + * @returns {T} + * + * @throws {NotFoundException} + * @throws {ContainerException} + */ + make< + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(identifier: Identifier, args?: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */): T + { + // TODO: Implement this... + return null as T; + } + + /** + * Resolves values if a binding exists for identifier, or returns a default value + * + * @template T = any + * @template D = undefined + * + * @param {Identifier} identifier + * @param {any[]} [args] Eventual arguments to pass on to {@link FactoryCallback} or {@link Constructor} + * @param {D} [defaultValue] + * + * @returns {T} + * + * @throws {ContainerException} + */ + makeOrDefault< + T = any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + D = undefined + >(identifier: Identifier, args?: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */, defaultValue?: D): T | D + { + // TODO: Implement this... + return null as D; + } + + /** + * Instantiate a new instance of given concrete + * + * @template T = object + * + * @param {Constructor | Binding} concrete + * + * @returns {T} + * + * @throws {ContainerException} + */ + build(concrete: Constructor | Binding): T + { + // TODO: Implement this... + return null as T; + } + + /** + * Call given method and inject dependencies if needed + * + * @param {Callback | CallbackWrapper | ClassMethodReference} method + * @param {any[]} args + * + * @return {any} + * + * @throws {ContainerException} + */ + call(method: Callback | CallbackWrapper | ClassMethodReference, args: any[]): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + // TODO: Implement this... + return null; + } + + /** + * Extend the registered binding + * + * @param {Identifier} identifier + * @param {ExtendCallback} callback + * + * @return {this} + * + * @throws {TypeError} + * @throws {ContainerException} + */ + extend(identifier: Identifier, callback: ExtendCallback): this + { + // TODO: Implement this... + return this; + } + + /** + * Forget binding and resolved instance for given identifier + * + * @param {Identifier} identifier + * + * @returns {boolean} + */ + forget(identifier: Identifier): boolean + { + // TODO: Implement this... + return false; + } + + /** + * Flush container of all bindings and resolved instances + * + * @returns {void} + */ + flush(): void + { + // TODO: Implement this... + return; + } + + /** + * Determine if identifier has been resolved + * + * @param {Identifier} identifier + * + * @return {boolean} + */ + isResolved(identifier: Identifier): boolean + { + // TODO: Implement this... + return false; + } + + /** + * Register a callback to be invoked before a binding is resolved + * + * @param {Identifier} identifier + * @param {BeforeResolvedCallback} callback + * + * @return {this} + */ + before(identifier: Identifier, callback: BeforeResolvedCallback): this + { + // TODO: Implement this... + return this; + } + + /** + * Register a callback to be invoked after a binding has been resolved + * + * @param {Identifier} identifier + * @param {AfterResolvedCallback} callback + * + * @return {this} + */ + after(identifier: Identifier, callback: AfterResolvedCallback): this + { + // TODO: Implement this... + return this; + } +} \ No newline at end of file From e1da3ea83875f5b75083c8d2647f8b26d16033e5 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 14:34:24 +0100 Subject: [PATCH 102/224] Export service container --- packages/container/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/container/src/index.ts b/packages/container/src/index.ts index 1d72b54d..fa7b3a16 100644 --- a/packages/container/src/index.ts +++ b/packages/container/src/index.ts @@ -1,4 +1,6 @@ import BindingEntry from "./BindingEntry"; +import Container from "./Container"; export { - BindingEntry + BindingEntry, + Container } \ No newline at end of file From 34c7ac514046a6989baa9ae41a59e5809a829c09 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 15:06:39 +0100 Subject: [PATCH 103/224] Fix isBindingIdentifier() included in export --- packages/container/rollup.config.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/container/rollup.config.mjs b/packages/container/rollup.config.mjs index fb24e061..9a9b0fa1 100644 --- a/packages/container/rollup.config.mjs +++ b/packages/container/rollup.config.mjs @@ -6,6 +6,7 @@ export default createConfig({ '@aedart/contracts/container', '@aedart/contracts/support', '@aedart/contracts/support/arrays', + '@aedart/contracts/support/container', '@aedart/contracts/support/concerns', '@aedart/contracts/support/exceptions', '@aedart/contracts/support/meta', @@ -13,6 +14,7 @@ export default createConfig({ '@aedart/contracts/support/reflections', '@aedart/support', '@aedart/support/arrays', + '@aedart/support/container', '@aedart/support/concerns', '@aedart/support/exceptions', '@aedart/support/meta', From fe21d38b1ca076028bf024a30c020494c73b40f2 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 15:12:47 +0100 Subject: [PATCH 104/224] Add Container's getInstance() and setInstance() static methods --- packages/container/src/Container.ts | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 8ab8dfc5..5966c728 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -19,6 +19,43 @@ import type { CallbackWrapper } from "@aedart/contracts/support"; */ export default class Container implements ServiceContainerContract { + /** + * Singleton instance of the service container + * + * @type {ServiceContainerContract|null} + * + * @private + * + * @static + */ + static #instance: ServiceContainerContract | null = null; + + /** + * Returns the singleton instance of the service container + * + * @return {ServiceContainerContract|this} + */ + static getInstance(): ServiceContainerContract + { + if (this.#instance === null) { + this.setInstance(new this()); + } + + return this.#instance as ServiceContainerContract; + } + + /** + * Set the singleton instance of the service container + * + * @param {ServiceContainerContract | null} [container] + * + * @return {ServiceContainerContract | null} + */ + static setInstance(container: ServiceContainerContract | null = null): ServiceContainerContract | null + { + return this.#instance = container; + } + /** * Register a binding * From 9365bbf0f8ec6596d2749196f99117accfad42ab Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 15:12:58 +0100 Subject: [PATCH 105/224] Add test for container's singleton instance --- .../container/container/getInstance.test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/browser/packages/support/container/container/getInstance.test.js diff --git a/tests/browser/packages/support/container/container/getInstance.test.js b/tests/browser/packages/support/container/container/getInstance.test.js new file mode 100644 index 00000000..cf1b5685 --- /dev/null +++ b/tests/browser/packages/support/container/container/getInstance.test.js @@ -0,0 +1,18 @@ +import { Container } from "@aedart/container"; + +describe('@aedart/support/container', () => { + describe('getInstance', () => { + + it('returns singleton instance', () => { + const a = Container.getInstance(); + const b = Container.getInstance(); + + expect(b) + .withContext('Incorrect container instance returned') + .toBe(a); + + Container.setInstance(null); + }); + + }); +}); \ No newline at end of file From 4da5eaf36c9b68ab70f96bd85ecd6aed6e122865 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 15:37:38 +0100 Subject: [PATCH 106/224] Change container interface, add isAlias() method --- packages/contracts/src/container/Container.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index b3554423..3ff49f80 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -134,6 +134,15 @@ export default interface Container */ alias(identifier: Identifier, alias: Alias): this; + /** + * Determine if identifier is an alias + * + * @param {Identifier} identifier + * + * @return {boolean} + */ + isAlias(identifier: Identifier): boolean; + /** * Resolves binding value that matches identifier and returns it * From 9629ae3aab018087ac686c7b82da1671987b6678 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 15:42:37 +0100 Subject: [PATCH 107/224] Add implementation of bind, bindIf, singleton, singletonIf, ... etc Container implementation is still far from complete, but this is a start. --- packages/container/src/Container.ts | 104 ++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 14 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 5966c728..25a61992 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -9,6 +9,7 @@ import type { } from "@aedart/contracts/container"; import type { Callback, ClassMethodReference, Constructor } from "@aedart/contracts"; import type { CallbackWrapper } from "@aedart/contracts/support"; +import BindingEntry from "./BindingEntry"; /** * Service Container @@ -30,6 +31,33 @@ export default class Container implements ServiceContainerContract */ static #instance: ServiceContainerContract | null = null; + /** + * Registered bindings + * + * @type {Map} + * + * @protected + */ + protected bindings: Map = new Map(); + + /** + * Registered aliases + * + * @type {Map} + * + * @protected + */ + protected aliases: Map = new Map(); + + /** + * Registered "shared" instances (singletons) + * + * @type {Map} + * + * @protected + */ + protected instances: Map = new Map(); + /** * Returns the singleton instance of the service container * @@ -67,9 +95,14 @@ export default class Container implements ServiceContainerContract * * @throws {TypeError} */ - bind(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared?: boolean): this + bind(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared: boolean = false): this { - // TODO: Implement this... + concrete = concrete ?? identifier as Constructor; + + this.bindings.set(identifier, this.makeBindingEntry(identifier, concrete, shared)); + + // TODO: isResolved() vs. rebound() ??? + return this; } @@ -84,9 +117,12 @@ export default class Container implements ServiceContainerContract * * @throws {TypeError} */ - bindIf(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared?: boolean): this + bindIf(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared: boolean = false): this { - // TODO: Implement this... + if (!this.bound(identifier)) { + this.bind(identifier, concrete, shared); + } + return this; } @@ -102,8 +138,7 @@ export default class Container implements ServiceContainerContract */ singleton(identifier: Identifier, concrete?: FactoryCallback | Constructor): this { - // TODO: Implement this... - return this; + return this.bind(identifier, concrete, true); } /** @@ -118,7 +153,10 @@ export default class Container implements ServiceContainerContract */ singletonIf(identifier: Identifier, concrete?: FactoryCallback | Constructor): this { - // TODO: Implement this... + if (!this.bound(identifier)) { + this.singleton(identifier, concrete); + } + return this; } @@ -136,8 +174,11 @@ export default class Container implements ServiceContainerContract */ instance(identifier: Identifier, instance: T): T { - // TODO: Implement this... - return null as T; + this.instances.set(identifier, instance as object); + + // TODO: rebound() ??? + + return instance; } /** @@ -169,8 +210,9 @@ export default class Container implements ServiceContainerContract */ has(identifier: Identifier): boolean { - // TODO: Implement this... - return false; + return this.bindings.has(identifier) + || this.instances.has(identifier) + || this.isAlias(identifier); } /** @@ -182,8 +224,7 @@ export default class Container implements ServiceContainerContract */ bound(identifier: Identifier): boolean { - // TODO: Implement this... - return false; + return this.has(identifier); } /** @@ -198,10 +239,27 @@ export default class Container implements ServiceContainerContract */ alias(identifier: Identifier, alias: Alias): this { - // TODO: Implement this... + if (alias === identifier) { + throw new TypeError(`${identifier.toString()} is aliased to itself`, { cause: { identifier: identifier, alias: alias } }); + } + + this.aliases.set(alias, identifier); + return this; } + /** + * Determine if identifier is an alias + * + * @param {Identifier} identifier + * + * @return {boolean} + */ + isAlias(identifier: Identifier): boolean + { + return this.aliases.has(identifier); + } + /** * Resolves binding value that matches identifier and returns it * @@ -360,4 +418,22 @@ export default class Container implements ServiceContainerContract // TODO: Implement this... return this; } + + /** + * Returns a new Binding Entry for given identifier and binding value + * + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} value + * @param {boolean} [shared] + * + * @return {Binding} + * + * @throws {TypeError} + * + * @protected + */ + protected makeBindingEntry(identifier: Identifier, value: FactoryCallback | Constructor, shared: boolean = false): Binding + { + return new BindingEntry(identifier, value, shared); + } } \ No newline at end of file From 424eac41f3775da2c2175f9b5da98bc271e27903 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 22 Mar 2024 15:45:05 +0100 Subject: [PATCH 108/224] Set public visibility on methods that container interface exposes --- packages/container/src/Container.ts | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 25a61992..c6246544 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -63,7 +63,7 @@ export default class Container implements ServiceContainerContract * * @return {ServiceContainerContract|this} */ - static getInstance(): ServiceContainerContract + public static getInstance(): ServiceContainerContract { if (this.#instance === null) { this.setInstance(new this()); @@ -79,7 +79,7 @@ export default class Container implements ServiceContainerContract * * @return {ServiceContainerContract | null} */ - static setInstance(container: ServiceContainerContract | null = null): ServiceContainerContract | null + public static setInstance(container: ServiceContainerContract | null = null): ServiceContainerContract | null { return this.#instance = container; } @@ -95,7 +95,7 @@ export default class Container implements ServiceContainerContract * * @throws {TypeError} */ - bind(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared: boolean = false): this + public bind(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared: boolean = false): this { concrete = concrete ?? identifier as Constructor; @@ -117,7 +117,7 @@ export default class Container implements ServiceContainerContract * * @throws {TypeError} */ - bindIf(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared: boolean = false): this + public bindIf(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared: boolean = false): this { if (!this.bound(identifier)) { this.bind(identifier, concrete, shared); @@ -136,7 +136,7 @@ export default class Container implements ServiceContainerContract * * @throws {TypeError} */ - singleton(identifier: Identifier, concrete?: FactoryCallback | Constructor): this + public singleton(identifier: Identifier, concrete?: FactoryCallback | Constructor): this { return this.bind(identifier, concrete, true); } @@ -151,7 +151,7 @@ export default class Container implements ServiceContainerContract * * @throws {TypeError} */ - singletonIf(identifier: Identifier, concrete?: FactoryCallback | Constructor): this + public singletonIf(identifier: Identifier, concrete?: FactoryCallback | Constructor): this { if (!this.bound(identifier)) { this.singleton(identifier, concrete); @@ -172,7 +172,7 @@ export default class Container implements ServiceContainerContract * * @throws {TypeError} */ - instance(identifier: Identifier, instance: T): T + public instance(identifier: Identifier, instance: T): T { this.instances.set(identifier, instance as object); @@ -193,7 +193,7 @@ export default class Container implements ServiceContainerContract * @throws {NotFoundException} * @throws {ContainerException} */ - get< + public get< T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ >(identifier: Identifier): T { @@ -208,7 +208,7 @@ export default class Container implements ServiceContainerContract * * @returns {boolean} */ - has(identifier: Identifier): boolean + public has(identifier: Identifier): boolean { return this.bindings.has(identifier) || this.instances.has(identifier) @@ -222,7 +222,7 @@ export default class Container implements ServiceContainerContract * * @returns {boolean} */ - bound(identifier: Identifier): boolean + public bound(identifier: Identifier): boolean { return this.has(identifier); } @@ -237,7 +237,7 @@ export default class Container implements ServiceContainerContract * * @throws {TypeError} */ - alias(identifier: Identifier, alias: Alias): this + public alias(identifier: Identifier, alias: Alias): this { if (alias === identifier) { throw new TypeError(`${identifier.toString()} is aliased to itself`, { cause: { identifier: identifier, alias: alias } }); @@ -255,7 +255,7 @@ export default class Container implements ServiceContainerContract * * @return {boolean} */ - isAlias(identifier: Identifier): boolean + public isAlias(identifier: Identifier): boolean { return this.aliases.has(identifier); } @@ -273,7 +273,7 @@ export default class Container implements ServiceContainerContract * @throws {NotFoundException} * @throws {ContainerException} */ - make< + public make< T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ >(identifier: Identifier, args?: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */): T { @@ -295,7 +295,7 @@ export default class Container implements ServiceContainerContract * * @throws {ContainerException} */ - makeOrDefault< + public makeOrDefault< T = any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ D = undefined >(identifier: Identifier, args?: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */, defaultValue?: D): T | D @@ -315,7 +315,7 @@ export default class Container implements ServiceContainerContract * * @throws {ContainerException} */ - build(concrete: Constructor | Binding): T + public build(concrete: Constructor | Binding): T { // TODO: Implement this... return null as T; @@ -331,7 +331,7 @@ export default class Container implements ServiceContainerContract * * @throws {ContainerException} */ - call(method: Callback | CallbackWrapper | ClassMethodReference, args: any[]): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + public call(method: Callback | CallbackWrapper | ClassMethodReference, args: any[]): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { // TODO: Implement this... return null; @@ -348,7 +348,7 @@ export default class Container implements ServiceContainerContract * @throws {TypeError} * @throws {ContainerException} */ - extend(identifier: Identifier, callback: ExtendCallback): this + public extend(identifier: Identifier, callback: ExtendCallback): this { // TODO: Implement this... return this; @@ -361,7 +361,7 @@ export default class Container implements ServiceContainerContract * * @returns {boolean} */ - forget(identifier: Identifier): boolean + public forget(identifier: Identifier): boolean { // TODO: Implement this... return false; @@ -372,7 +372,7 @@ export default class Container implements ServiceContainerContract * * @returns {void} */ - flush(): void + public flush(): void { // TODO: Implement this... return; @@ -385,7 +385,7 @@ export default class Container implements ServiceContainerContract * * @return {boolean} */ - isResolved(identifier: Identifier): boolean + public isResolved(identifier: Identifier): boolean { // TODO: Implement this... return false; @@ -399,7 +399,7 @@ export default class Container implements ServiceContainerContract * * @return {this} */ - before(identifier: Identifier, callback: BeforeResolvedCallback): this + public before(identifier: Identifier, callback: BeforeResolvedCallback): this { // TODO: Implement this... return this; @@ -413,7 +413,7 @@ export default class Container implements ServiceContainerContract * * @return {this} */ - after(identifier: Identifier, callback: AfterResolvedCallback): this + public after(identifier: Identifier, callback: AfterResolvedCallback): this { // TODO: Implement this... return this; From 77818f7ac5978fe72492c33b2f6e96455915396b Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 22 Mar 2024 18:11:36 +0100 Subject: [PATCH 109/224] Add isShared() --- packages/contracts/src/container/Container.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index 3ff49f80..d9e68594 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -142,6 +142,15 @@ export default interface Container * @return {boolean} */ isAlias(identifier: Identifier): boolean; + + /** + * Determine if identifier is registered as a "shared" binding + * + * @param {Identifier} identifier + * + * @returns {boolean} + */ + isShared(identifier: Identifier): boolean; /** * Resolves binding value that matches identifier and returns it From 1b26f802f5567c9deabaff20cdaa6d8df81cb374 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 22 Mar 2024 18:26:00 +0100 Subject: [PATCH 110/224] Add container related errors / exceptions --- .../src/exceptions/ContainerError.ts | 23 +++++++++++++++++++ .../container/src/exceptions/NotFoundError.ts | 21 +++++++++++++++++ packages/container/src/exceptions/index.ts | 6 +++++ packages/container/src/index.ts | 4 +++- 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 packages/container/src/exceptions/ContainerError.ts create mode 100644 packages/container/src/exceptions/NotFoundError.ts create mode 100644 packages/container/src/exceptions/index.ts diff --git a/packages/container/src/exceptions/ContainerError.ts b/packages/container/src/exceptions/ContainerError.ts new file mode 100644 index 00000000..d62d1751 --- /dev/null +++ b/packages/container/src/exceptions/ContainerError.ts @@ -0,0 +1,23 @@ +import type { ContainerException } from "@aedart/contracts/container"; +import { configureCustomError } from "@aedart/support/exceptions"; + +/** + * Container Error + * + * @see ContainerException + */ +export default class ContainerError extends Error implements ContainerException +{ + /** + * Create new Container Error instance + * + * @param {string} [message] + * @param {ErrorOptions} [options] + */ + constructor(message?: string, options?: ErrorOptions) + { + super(message, options); + + configureCustomError(this); + } +} \ No newline at end of file diff --git a/packages/container/src/exceptions/NotFoundError.ts b/packages/container/src/exceptions/NotFoundError.ts new file mode 100644 index 00000000..4ea9c3ec --- /dev/null +++ b/packages/container/src/exceptions/NotFoundError.ts @@ -0,0 +1,21 @@ +import type { NotFoundException } from "@aedart/contracts/container"; +import ContainerError from "./ContainerError"; + +/** + * Not Found Error + * + * @see NotFoundException + */ +export default class NotFoundError extends ContainerError implements NotFoundException +{ + /** + * Create new Not Found Error instance + * + * @param {string} [message] + * @param {ErrorOptions} [options] + */ + constructor(message?: string, options?: ErrorOptions) + { + super(message, options); + } +} \ No newline at end of file diff --git a/packages/container/src/exceptions/index.ts b/packages/container/src/exceptions/index.ts new file mode 100644 index 00000000..73ee0de1 --- /dev/null +++ b/packages/container/src/exceptions/index.ts @@ -0,0 +1,6 @@ +import ContainerError from "./ContainerError"; +import NotFoundError from "./NotFoundError"; +export { + ContainerError, + NotFoundError +} \ No newline at end of file diff --git a/packages/container/src/index.ts b/packages/container/src/index.ts index fa7b3a16..6e107f9a 100644 --- a/packages/container/src/index.ts +++ b/packages/container/src/index.ts @@ -3,4 +3,6 @@ import Container from "./Container"; export { BindingEntry, Container -} \ No newline at end of file +} + +export * from './exceptions'; \ No newline at end of file From 7f0fef09d43bad92c77460112b87ea1c7602ffa0 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 22 Mar 2024 18:48:59 +0100 Subject: [PATCH 111/224] Change build, allow argument overwrites --- packages/contracts/src/container/Container.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index d9e68594..104d43dd 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -194,12 +194,16 @@ export default interface Container * @template T = object * * @param {Constructor | Binding} concrete + * @param {any[]} [args] Eventual arguments to pass on the concrete instance's constructor. * * @returns {T} * * @throws {ContainerException} */ - build(concrete: Constructor | Binding): T; + build( + concrete: Constructor | Binding, + args?: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): T; /** * Call given method and inject dependencies if needed From 9193efcddfe9d746291882c5537914b850818e2b Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Fri, 22 Mar 2024 20:14:10 +0100 Subject: [PATCH 112/224] Add implementation for make() and resolve() --- packages/container/src/Container.ts | 298 ++++++++++++++++++++++++++-- 1 file changed, 281 insertions(+), 17 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index c6246544..c1a19d77 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -9,6 +9,9 @@ import type { } from "@aedart/contracts/container"; import type { Callback, ClassMethodReference, Constructor } from "@aedart/contracts"; import type { CallbackWrapper } from "@aedart/contracts/support"; +import { isConstructor } from "@aedart/support/reflections"; +import ContainerError from "./exceptions/ContainerError"; +import NotFoundError from "./exceptions/NotFoundError"; import BindingEntry from "./BindingEntry"; /** @@ -58,6 +61,41 @@ export default class Container implements ServiceContainerContract */ protected instances: Map = new Map(); + /** + * Extend callbacks + * + * @type {Map} + * + * @protected + */ + protected extenders: Map = new Map(); + + /** + * Resolved (built) identifiers + * + * @type {Set} + * + * @protected + */ + protected resolved: Set = new Set(); + + /** + * "Before" resolved callbacks + * + * @type {Map} + * + * @protected + */ + protected beforeResolvedCallbacks: Map = new Map(); + + /** + * "After" resolved callbacks + * + * @type {Map} + * @protected + */ + protected afterResolvedCallbacks: Map = new Map(); + /** * Returns the singleton instance of the service container * @@ -197,8 +235,7 @@ export default class Container implements ServiceContainerContract T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ >(identifier: Identifier): T { - // TODO: Implement this... - return null as T; + return this.make(identifier); } /** @@ -259,6 +296,33 @@ export default class Container implements ServiceContainerContract { return this.aliases.has(identifier); } + + /** + * Determine if identifier is registered as a "shared" binding + * + * @param {Identifier} identifier + * + * @returns {boolean} + */ + public isShared(identifier: Identifier): boolean + { + return this.instances.has(identifier) + || (this.bindings.has(identifier) && (this.bindings.get(identifier) as Binding).shared) + } + + /** + * Returns the alias for given identifier, if available + * + * @param {Identifier} identifier + * + * @returns {Identifier} + */ + public getAlias(identifier: Identifier): Identifier + { + return this.aliases.has(identifier) + ? this.getAlias(this.aliases.get(identifier) as Identifier) + : identifier; + } /** * Resolves binding value that matches identifier and returns it @@ -275,10 +339,9 @@ export default class Container implements ServiceContainerContract */ public make< T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ - >(identifier: Identifier, args?: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */): T + >(identifier: Identifier, args: any[] = [] /* eslint-disable-line @typescript-eslint/no-explicit-any */): T { - // TODO: Implement this... - return null as T; + return this.resolve(identifier, args); } /** @@ -300,8 +363,15 @@ export default class Container implements ServiceContainerContract D = undefined >(identifier: Identifier, args?: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */, defaultValue?: D): T | D { - // TODO: Implement this... - return null as D; + if (!this.has(identifier) && !this.isBuildable(identifier)) { + if (typeof defaultValue === 'function') { + return defaultValue(args, this); + } + + return defaultValue as D; + } + + return this.make(identifier, args); } /** @@ -310,12 +380,16 @@ export default class Container implements ServiceContainerContract * @template T = object * * @param {Constructor | Binding} concrete + * @param {any[]} [args] Eventual arguments to pass on the concrete instance's constructor. * * @returns {T} * * @throws {ContainerException} */ - public build(concrete: Constructor | Binding): T + public build( + concrete: Constructor | Binding, + args: any[] = [] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): T { // TODO: Implement this... return null as T; @@ -350,7 +424,25 @@ export default class Container implements ServiceContainerContract */ public extend(identifier: Identifier, callback: ExtendCallback): this { - // TODO: Implement this... + identifier = this.getAlias(identifier); + + // If identifier matches a "shared" instance, then extend that instance right + // away and rebound it. + if (this.instances.has(identifier)) { + const instance = this.instances.get(identifier) as object; + + this.instances.set(identifier, callback(instance, this)); + } else { + // Otherwise, add extend callback to the existing for the identifier. + if (!this.extenders.has(identifier)) { + this.extenders.set(identifier, []); + } + + const existing: ExtendCallback[] = this.extenders.get(identifier) as ExtendCallback[]; + existing.push(callback); + this.extenders.set(identifier, existing); + } + return this; } @@ -363,8 +455,13 @@ export default class Container implements ServiceContainerContract */ public forget(identifier: Identifier): boolean { - // TODO: Implement this... - return false; + const alias: Identifier = this.getAlias(identifier); + + const removedBinding: boolean = this.bindings.delete(alias); + const removedInstance: boolean = this.instances.delete(alias); + const removedAlias: boolean = this.aliases.delete(identifier); + + return removedBinding || removedInstance || removedAlias; } /** @@ -374,8 +471,10 @@ export default class Container implements ServiceContainerContract */ public flush(): void { - // TODO: Implement this... - return; + this.bindings.clear(); + this.instances.clear(); + this.aliases.clear(); + this.resolved.clear(); } /** @@ -387,8 +486,10 @@ export default class Container implements ServiceContainerContract */ public isResolved(identifier: Identifier): boolean { - // TODO: Implement this... - return false; + identifier = this.getAlias(identifier); + + return this.resolved.has(identifier) + || this.instances.has(identifier); } /** @@ -401,7 +502,16 @@ export default class Container implements ServiceContainerContract */ public before(identifier: Identifier, callback: BeforeResolvedCallback): this { - // TODO: Implement this... + identifier = this.getAlias(identifier); + + if (!this.beforeResolvedCallbacks.has(identifier)) { + this.beforeResolvedCallbacks.set(identifier, []); + } + + const existing: BeforeResolvedCallback[] = this.beforeResolvedCallbacks.get(identifier) as BeforeResolvedCallback[]; + existing.push(callback); + this.beforeResolvedCallbacks.set(identifier, existing); + return this; } @@ -415,7 +525,16 @@ export default class Container implements ServiceContainerContract */ public after(identifier: Identifier, callback: AfterResolvedCallback): this { - // TODO: Implement this... + identifier = this.getAlias(identifier); + + if (!this.afterResolvedCallbacks.has(identifier)) { + this.afterResolvedCallbacks.set(identifier, []); + } + + const existing: AfterResolvedCallback[] = this.afterResolvedCallbacks.get(identifier) as AfterResolvedCallback[]; + existing.push(callback); + this.afterResolvedCallbacks.set(identifier, existing); + return this; } @@ -436,4 +555,149 @@ export default class Container implements ServiceContainerContract { return new BindingEntry(identifier, value, shared); } + + /** + * Resolves binding value that matches identifier + * + * @template T = any + * + * @param {Identifier} identifier + * @param {any[]} args Eventual arguments to pass on to {@link FactoryCallback} or {@link Constructor} + * @param {boolean} [fireEvents=true] If `true`, then "before" and "after" resolved callbacks will be invoked. + * + * @returns {T} + * + * @throws {NotFoundException} + * @throws {ContainerException} + * + * @protected + */ + protected resolve< + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >( + identifier: Identifier, + args: any[] = [], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + fireEvents: boolean = true + ): T + { + identifier = this.getAlias(identifier); + + // Fire "before" resolve callbacks + if (fireEvents) { + this.fireBeforeResolvedCallbacks(identifier, args); + } + + // Returns "shared" instance, if requested identifier matches one. + if (this.instances.has(identifier)) { + return this.instances.get(identifier) as T; + } + + // Find registered binding for identifier. Or, fail if no binding exists and given + // identifier is not buildable. + const binding: Binding | undefined = this.bindings.get(identifier); + + if (binding === undefined && this.isBuildable(identifier)) { + return this.build(identifier as Constructor, args); + } else if (binding === undefined) { + throw new NotFoundError(`No binding found for ${identifier.toString()}`, { cause: { identifier: identifier, args: args } }); + } + + // Build instance or value that was registered for the given identifier. + let resolved: T = this.build(binding, args); + + // Invoke all extend callbacks for identifier, if any have been registered. + const extendCallbacks: ExtendCallback[] = this.getExtenders(identifier); + for (const extendCallback of extendCallbacks) { + resolved = extendCallback(resolved, this) as T; + } + + // If requested binding is registered as "shared", save the resolved as an instance. + if (this.isShared(identifier)) { + this.instances.set(identifier, resolved as object); + } + + // Fire "after" resolved callbacks + if (fireEvents) { + this.fireAfterResolvedCallbacks(identifier, resolved); + } + + // Mark identifier as resolved + this.resolved.add(identifier); + + // Finally, return the resolved instance or value + return resolved; + } + + /** + * Invokes the "before" resolved callbacks for given identifier + * + * @param {Identifier} identifier + * @param {any[]} [args] + * + * @return {void} + * + * @protected + */ + protected fireBeforeResolvedCallbacks( + identifier: Identifier, + args: any[] = [], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): void + { + const callbacks: BeforeResolvedCallback[] = this.beforeResolvedCallbacks.get(identifier) ?? []; + + for (const callback of callbacks) { + callback(identifier, args, this); + } + } + + /** + * Invokes the "after" resolved callbacks for given identifier + * + * @param {Identifier} identifier + * + * @param {any} resolved + * + * @return {void} + * + * @protected + */ + protected fireAfterResolvedCallbacks( + identifier: Identifier, + resolved: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): void + { + const callbacks: AfterResolvedCallback[] = this.afterResolvedCallbacks.get(identifier) ?? []; + + for (const callback of callbacks) { + callback(identifier, resolved, this); + } + } + + /** + * Returns list of extend callbacks for given identifier + * + * @param {Identifier} identifier + * + * @returns {ExtendCallback[]} + * + * @protected + */ + protected getExtenders(identifier: Identifier): ExtendCallback[] + { + return this.extenders.get(this.getAlias(identifier)) ?? []; + } + + /** + * Determine if given target is buildable + * + * @param {unknown} target + * + * @returns {boolean} + * + * @protected + */ + protected isBuildable(target: unknown): boolean + { + return isConstructor(target); + } } \ No newline at end of file From a442868e59543075e08551ccebe373bc3006f325 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 23 Mar 2024 21:13:20 +0100 Subject: [PATCH 113/224] Add isBinding() util --- .../container/src/BindingEntryBlueprint.ts | 19 ++++++++ packages/container/src/index.ts | 4 +- packages/container/src/isBinding.ts | 28 +++++++++++ .../packages/container/isBinding.test.js | 46 +++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 packages/container/src/BindingEntryBlueprint.ts create mode 100644 packages/container/src/isBinding.ts create mode 100644 tests/browser/packages/container/isBinding.test.js diff --git a/packages/container/src/BindingEntryBlueprint.ts b/packages/container/src/BindingEntryBlueprint.ts new file mode 100644 index 00000000..0f284602 --- /dev/null +++ b/packages/container/src/BindingEntryBlueprint.ts @@ -0,0 +1,19 @@ +import type { ClassBlueprint } from "@aedart/contracts/support/reflections"; + +/** + * Binding Entry Blueprint + * + * Defines the minimum members that a target class should contain, before it is + * considered to "look like" a [Binding]{@link import('@aedart/contracts/container').Binding} + * + * @type {ClassBlueprint} + */ +export const BindingEntryBlueprint: ClassBlueprint = { + members: [ + 'identifier', + 'value', + 'shared', + 'isFactoryCallback', + 'isConstructor' + ] +}; \ No newline at end of file diff --git a/packages/container/src/index.ts b/packages/container/src/index.ts index 6e107f9a..906cf316 100644 --- a/packages/container/src/index.ts +++ b/packages/container/src/index.ts @@ -5,4 +5,6 @@ export { Container } -export * from './exceptions'; \ No newline at end of file +export * from './exceptions'; +export * from './BindingEntryBlueprint'; +export * from './isBinding'; \ No newline at end of file diff --git a/packages/container/src/isBinding.ts b/packages/container/src/isBinding.ts new file mode 100644 index 00000000..aa0dae8d --- /dev/null +++ b/packages/container/src/isBinding.ts @@ -0,0 +1,28 @@ +import { classLooksLike } from "@aedart/support/reflections"; +import { isset } from "@aedart/support/misc"; +import { BindingEntryBlueprint } from "./BindingEntryBlueprint"; +import BindingEntry from "./BindingEntry"; + +/** + * Determine if given object is a [Binding]{@link import('@aedart/contracts/container').Binding} + * + * @param {object} instance + * + * @returns {boolean} + */ +export function isBinding(instance: object): boolean +{ + if (!isset(instance) || typeof instance !== 'object') { + return false; + } + + if (instance instanceof BindingEntry) { + return true; + } + + if (!Reflect.has(instance, 'constructor')) { + return false; + } + + return classLooksLike(instance.constructor as object, BindingEntryBlueprint); +} \ No newline at end of file diff --git a/tests/browser/packages/container/isBinding.test.js b/tests/browser/packages/container/isBinding.test.js new file mode 100644 index 00000000..d2027392 --- /dev/null +++ b/tests/browser/packages/container/isBinding.test.js @@ -0,0 +1,46 @@ +import { isBinding, BindingEntry } from "@aedart/container"; + +describe('@aedart/container', () => { + describe('isBinding', () => { + + it('can determine if target is a binding entry', () => { + + class A { + get identifier() { return true; } + get value() { return true; } + get shared() { return true; } + get isFactoryCallback() { return true; } + get isConstructor() { return true; } + } + + const obj = { + 'identifier': true, + 'value': true, + //'shared': true, // this should cause negative result... + 'isFactoryCallback': true, + 'isConstructor': true + }; + + const data = [ + { value: undefined, expected: false, name: 'undefined' }, + { value: null, expected: false, name: 'null' }, + { value: {}, expected: false, name: 'object (empty)' }, + { value: [], expected: false, name: 'array (empty)' }, + + { value: obj, expected: false, name: 'object that "almost" looks like a binding entry' }, + + { value: new BindingEntry('a', () => true), expected: true, name: 'Binding Entry class (object)' }, + { value: new A(), expected: true, name: 'Custom Binding Entry class (object)' }, + ]; + + data.forEach((entry, index) => { + + let result = isBinding(entry.value); + expect(result) + .withContext(`${entry.name} was expected to be ${entry.expected}`) + .toBe(entry.expected); + }); + }); + + }); +}); \ No newline at end of file From a59a52210f95aa70b77f7568224d4452631a161b Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sat, 23 Mar 2024 22:21:44 +0100 Subject: [PATCH 114/224] Add build() implementation Still missing the resolving of dependencies for constructors, at this point. --- packages/container/src/Container.ts | 189 +++++++++++++++++++++++++++- 1 file changed, 185 insertions(+), 4 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index c1a19d77..9b389900 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -7,12 +7,20 @@ import type { Identifier, Binding } from "@aedart/contracts/container"; -import type { Callback, ClassMethodReference, Constructor } from "@aedart/contracts"; +import type { + Callback, + ClassMethodReference, + Constructor, + ConstructorLike +} from "@aedart/contracts"; import type { CallbackWrapper } from "@aedart/contracts/support"; -import { isConstructor } from "@aedart/support/reflections"; +import { hasDependencies, getDependencies } from "@aedart/support/container"; +import { getErrorMessage } from "@aedart/support/exceptions"; +import { isConstructor, getNameOrDesc } from "@aedart/support/reflections"; import ContainerError from "./exceptions/ContainerError"; import NotFoundError from "./exceptions/NotFoundError"; import BindingEntry from "./BindingEntry"; +import { isBinding } from "./isBinding"; /** * Service Container @@ -95,6 +103,15 @@ export default class Container implements ServiceContainerContract * @protected */ protected afterResolvedCallbacks: Map = new Map(); + + /** + * Resolve stack + * + * @type {Set} + * + * @protected + */ + protected resolveStack: Set = new Set(); /** * Returns the singleton instance of the service container @@ -391,8 +408,31 @@ export default class Container implements ServiceContainerContract args: any[] = [] /* eslint-disable-line @typescript-eslint/no-explicit-any */ ): T { - // TODO: Implement this... - return null as T; + const isBinding: boolean = this.isBinding(concrete); + let identifier: Identifier = 'unknown'; + + // Resolve factory callback, when binding is given of such type. + if (isBinding && (concrete as Binding).isFactoryCallback()) { + return this.resolveFactoryCallback((concrete as Binding).value as FactoryCallback, args, (concrete as Binding).identifier); + } + + // Extract constructor, if concrete is a binding so that it can be resolved. + if (isBinding) { + identifier = (concrete as Binding).identifier; + concrete = (concrete as Binding).value as Constructor; + } + + // Abort if concrete is not buildable + if (!this.isBuildable(concrete)) { + throw new ContainerError(`Unable to build concrete instance`, { cause: { concrete: concrete, args: args } }); + } + + // Resolve the constructor and eventual dependencies, when no arguments are given. + return this.resolveConstructor( + concrete as Constructor, + args, + identifier + ); } /** @@ -475,6 +515,7 @@ export default class Container implements ServiceContainerContract this.instances.clear(); this.aliases.clear(); this.resolved.clear(); + this.resolveStack.clear(); } /** @@ -628,6 +669,104 @@ export default class Container implements ServiceContainerContract return resolved; } + /** + * Resolves given factory callback + * + * @template T = object + * + * @param {FactoryCallback} callback + * @param {any[]} [args] + * @param {Identifier} [identifier] + * + * @returns {T} + * + * @throws {ContainerException} + * + * @protected + */ + protected resolveFactoryCallback( + callback: FactoryCallback, + args: any[] = [], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + identifier: Identifier = 'unknown' + ): T + { + try { + return callback(this, ...args) as T; + } catch (e) { + const reason: string = getErrorMessage(e); + const options = { + cause: { + callback: callback, + args: args, + identifier: identifier, + previous: e + } + } + + throw new ContainerError(`Unable to resolve factory callback for binding "${identifier.toString()}": ${reason}`, options); + } + } + + /** + * Resolves given constructor and eventual dependencies + * + * @template T = object + * + * @param {Constructor} target + * @param {any[]} [args] Defaults to target class' dependencies (if available), when no arguments are + * given. + * @param {Identifier} [identifier] + * + * @returns {T} + * + * @throws {ContainerException} + * + * @protected + */ + protected resolveConstructor( + target: Constructor, + args: any[] = [], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + identifier: Identifier = 'unknown' + ): T + { + try { + // Prevent circular dependency... + if (this.resolveStack.has(target as Constructor)) { + throw new ContainerError(`Circular Dependency for target "${getNameOrDesc(target as ConstructorLike)}"`); + } + + this.resolveStack.add(target as Constructor); + + // When no arguments are given (overwrites), attempt to obtain defined dependencies + // for the target class. + if (args.length == 0 && this.hasDependencies(target)) { + // TODO: Obtain dependencies... + } + + // Create the instance with arguments. + const resolved: T = new target(...args) as T; + + this.resolveStack.delete(target as Constructor); + + return resolved; + } catch (e) { + const reason: string = getErrorMessage(e); + const options = { + cause: { + target: target, + args: args, + identifier: identifier, + resolveStack: Array.from(this.resolveStack), + previous: e + } + } + + this.resolveStack.delete(target as Constructor); + + throw new ContainerError(`Unable to resolve "${getNameOrDesc(target as ConstructorLike)}" for binding "${identifier.toString()}": ${reason}`, options); + } + } + /** * Invokes the "before" resolved callbacks for given identifier * @@ -700,4 +839,46 @@ export default class Container implements ServiceContainerContract { return isConstructor(target); } + + /** + * Determine if object is a binding entry + * + * @param {object} target + * + * @returns {boolean} + * + * @protected + */ + protected isBinding(target: object): boolean + { + return isBinding(target); + } + + /** + * Determine if target has dependencies defined + * + * @param {object} target + * + * @returns {boolean} + * + * @protected + */ + protected hasDependencies(target: object): boolean + { + return hasDependencies(target); + } + + /** + * Returns the defined dependencies for given target + * + * @param {object} target + * + * @returns {Identifier[]} + * + * @protected + */ + protected getDependencies(target: object): Identifier[] + { + return getDependencies(target); + } } \ No newline at end of file From f293b71443ba3171af0108c993c2b0aac9a6c86a Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 24 Mar 2024 17:12:12 +0100 Subject: [PATCH 115/224] Add resolve dependencies logic Container should now be able to resolve a target's dependencies, if any are defined when resolving without argument overwrites. --- packages/container/src/Container.ts | 103 +++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 10 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 9b389900..400aa183 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -409,7 +409,7 @@ export default class Container implements ServiceContainerContract ): T { const isBinding: boolean = this.isBinding(concrete); - let identifier: Identifier = 'unknown'; + let identifier: Identifier = concrete; // Resolve factory callback, when binding is given of such type. if (isBinding && (concrete as Binding).isFactoryCallback()) { @@ -687,12 +687,14 @@ export default class Container implements ServiceContainerContract protected resolveFactoryCallback( callback: FactoryCallback, args: any[] = [], /* eslint-disable-line @typescript-eslint/no-explicit-any */ - identifier: Identifier = 'unknown' + identifier?: Identifier ): T { try { return callback(this, ...args) as T; } catch (e) { + identifier = identifier ?? callback; + const reason: string = getErrorMessage(e); const options = { cause: { @@ -726,21 +728,18 @@ export default class Container implements ServiceContainerContract protected resolveConstructor( target: Constructor, args: any[] = [], /* eslint-disable-line @typescript-eslint/no-explicit-any */ - identifier: Identifier = 'unknown' + identifier?: Identifier ): T { try { - // Prevent circular dependency... - if (this.resolveStack.has(target as Constructor)) { - throw new ContainerError(`Circular Dependency for target "${getNameOrDesc(target as ConstructorLike)}"`); - } + this.preventCircularDependency(target as Constructor); this.resolveStack.add(target as Constructor); - // When no arguments are given (overwrites), attempt to obtain defined dependencies - // for the target class. + // When no arguments are given (overwrites), attempt to obtain defined + // dependencies for the target class and resolve them from the container. if (args.length == 0 && this.hasDependencies(target)) { - // TODO: Obtain dependencies... + args = this.resolveDependencies(target); } // Create the instance with arguments. @@ -750,6 +749,8 @@ export default class Container implements ServiceContainerContract return resolved; } catch (e) { + identifier = identifier ?? target; + const reason: string = getErrorMessage(e); const options = { cause: { @@ -767,6 +768,57 @@ export default class Container implements ServiceContainerContract } } + /** + * Obtains and resolves dependencies for given target + * + * @param {object} target + * @returns {any[]} Resolved dependencies or empty, if none available + * + * @throws {ContainerError} + * + * @protected + */ + protected resolveDependencies(target: object): any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + const resolved: any[] = []; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + const dependencies: Identifier[] = this.getDependencies(target); + for (const identifier of dependencies) { + resolved.push(this.resolveDependency(identifier, target)); + } + + return resolved; + } + + /** + * Resolves dependency that matches given identifier, for given target + * + * @param {Identifier} identifier + * @param {object} target + * + * @returns {any} + * + * @throws {ContainerError} + * + * @protected + */ + protected resolveDependency(identifier: Identifier, target: object): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + try { + return this.make(identifier); + } catch (e) { + const reason: string = getErrorMessage(e); + const options = { + cause: { + identifier: identifier, + target: target + } + } + + throw new ContainerError(`Unable to resolve "${identifier.toString()}" for target "${getNameOrDesc(target as ConstructorLike)}": ${reason}`, options); + } + } + /** * Invokes the "before" resolved callbacks for given identifier * @@ -881,4 +933,35 @@ export default class Container implements ServiceContainerContract { return getDependencies(target); } + + /** + * Aborts current make, build or call process, if given target is already in + * the process of being resolved. + * + * @param {Constructor} target + * + * @throws {ContainerError} + * + * @protected + */ + protected preventCircularDependency(target: Constructor): void + { + // Skip if target is not in the current "resolve" stack. + if (!this.resolveStack.has(target)) { + return; + } + + // However, if the target is in the current "resolve" stack, it means that the + // target has a circular dependency to itself. To avoid an infinite loop, the + // make, build or call process must be aborted. + + // Prepare a string "resolve" stack to make it somewhat easier for developers + // to understand how we got here... + const stack: string[] = Array.from(this.resolveStack).map((ctor: Constructor) => { + return getNameOrDesc(ctor); + }) + const trace: string = stack.join(' -> '); + + throw new ContainerError(`Circular Dependency for target "${getNameOrDesc(target as ConstructorLike)}": ${trace}`); + } } \ No newline at end of file From b4d6e6b62d32d43fd5b7d56402a2f89e23e5d108 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 24 Mar 2024 17:13:26 +0100 Subject: [PATCH 116/224] Reorder internals methods --- packages/container/src/Container.ts | 40 ++++++++++++++++------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 400aa183..f5afe3c9 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -579,24 +579,6 @@ export default class Container implements ServiceContainerContract return this; } - /** - * Returns a new Binding Entry for given identifier and binding value - * - * @param {Identifier} identifier - * @param {FactoryCallback | Constructor} value - * @param {boolean} [shared] - * - * @return {Binding} - * - * @throws {TypeError} - * - * @protected - */ - protected makeBindingEntry(identifier: Identifier, value: FactoryCallback | Constructor, shared: boolean = false): Binding - { - return new BindingEntry(identifier, value, shared); - } - /** * Resolves binding value that matches identifier * @@ -892,6 +874,28 @@ export default class Container implements ServiceContainerContract return isConstructor(target); } + /** + * Returns a new Binding Entry for given identifier and binding value + * + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} value + * @param {boolean} [shared] + * + * @return {Binding} + * + * @throws {TypeError} + * + * @protected + */ + protected makeBindingEntry( + identifier: Identifier, + value: FactoryCallback | Constructor, + shared: boolean = false + ): Binding + { + return new BindingEntry(identifier, value, shared); + } + /** * Determine if object is a binding entry * From b3f18b4126ba5e8149383c84122536db7bf56e4c Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 24 Mar 2024 18:33:04 +0100 Subject: [PATCH 117/224] Change args to be optional for call() --- packages/contracts/src/container/Container.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index 104d43dd..26224ac9 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -209,13 +209,13 @@ export default interface Container * Call given method and inject dependencies if needed * * @param {Callback | CallbackWrapper | ClassMethodReference} method - * @param {any[]} args + * @param {any[]} [args] * * @return {any} * * @throws {ContainerException} */ - call(method: Callback | CallbackWrapper | ClassMethodReference, args: any[]): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + call(method: Callback | CallbackWrapper | ClassMethodReference, args?: any[]): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ /** * Extend the registered binding From a5bd8255c3fabb60cc0a19533b070d5125534bd9 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 24 Mar 2024 19:06:03 +0100 Subject: [PATCH 118/224] Add call() logic --- packages/container/src/Container.ts | 129 ++++++++++++++++++++++++++-- 1 file changed, 122 insertions(+), 7 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index f5afe3c9..1ca07b0f 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -1,14 +1,17 @@ import type { AfterResolvedCallback, - Alias, BeforeResolvedCallback, + Alias, + BeforeResolvedCallback, Container as ServiceContainerContract, ExtendCallback, FactoryCallback, Identifier, - Binding + Binding, + DEPENDENCIES } from "@aedart/contracts/container"; import type { Callback, + ClassMethodName, ClassMethodReference, Constructor, ConstructorLike @@ -16,7 +19,13 @@ import type { import type { CallbackWrapper } from "@aedart/contracts/support"; import { hasDependencies, getDependencies } from "@aedart/support/container"; import { getErrorMessage } from "@aedart/support/exceptions"; -import { isConstructor, getNameOrDesc } from "@aedart/support/reflections"; +import { + isConstructor, + isClassMethodReference, + getNameOrDesc, + hasAllMethods, +} from "@aedart/support/reflections"; +import { isCallbackWrapper } from "@aedart/support"; import ContainerError from "./exceptions/ContainerError"; import NotFoundError from "./exceptions/NotFoundError"; import BindingEntry from "./BindingEntry"; @@ -439,16 +448,28 @@ export default class Container implements ServiceContainerContract * Call given method and inject dependencies if needed * * @param {Callback | CallbackWrapper | ClassMethodReference} method - * @param {any[]} args + * @param {any[]} [args] * * @return {any} * * @throws {ContainerException} */ - public call(method: Callback | CallbackWrapper | ClassMethodReference, args: any[]): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + public call(method: Callback | CallbackWrapper | ClassMethodReference, args: any[] = []): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { - // TODO: Implement this... - return null; + if (isClassMethodReference(method)) { + return this.invokeClassMethod(method as ClassMethodReference, args); + } + + if (isCallbackWrapper(method)) { + return this.invokeCallbackWrapper(method as CallbackWrapper, args); + } + + const type: string = typeof method; + if (type == 'function') { + return this.invokeCallback(method as Callback, args); + } + + throw new ContainerError(`Unable to call method: ${type} is not supported`, { cause: { method: method, args: args } }); } /** @@ -749,6 +770,100 @@ export default class Container implements ServiceContainerContract throw new ContainerError(`Unable to resolve "${getNameOrDesc(target as ConstructorLike)}" for binding "${identifier.toString()}": ${reason}`, options); } } + + /** + * Invokes method in class and returns the methods output + * + * @param {ClassMethodReference} reference + * @param {any[]} [args] + * + * @returns {any} + * + * @throws {ContainerError} + * + * @protected + */ + protected invokeClassMethod(reference: ClassMethodReference, args: any[] = []): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + // Build object, when target is a constructor + let target = reference[0]; + if (typeof target != 'object') { + target = this.make(target); + } + + const name: ClassMethodName = reference[1]; + const method: Callback = target[name]; + + return this.invokeCallback(method.bind(target), args); + } + + /** + * Invokes the wrapped callback and returns its output + * + * @param {CallbackWrapper} wrapper + * @param {any[]} [args] + * + * @returns {any} + * + * @throws {ContainerError} + * + * @protected + */ + protected invokeCallbackWrapper(wrapper: CallbackWrapper, args: any[] = []): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + // A callback wrapper might already have arguments defined. However, + // if there are any arguments provided here, then the wrapper's arguments must + // be overwritten. + if (args.length != 0) { + wrapper.arguments = args; + } + + // But, if no arguments are given, and if the wrapper does not have any arguments, + // then we check if "dependencies" has been defined. If so, they which must be resolved + // and set as arguments for the wrapper's callback. + if (args.length == 0 + && !wrapper.hasArguments() + && hasAllMethods(wrapper, 'has', 'get') + /* @ts-ignore */ + && wrapper['has'](DEPENDENCIES) + ) { + /* @ts-ignore */ + const dependencies: Identifier[] = wrapper['get']() ?? []; + const resolved: any[] = []; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + for(const identifier of dependencies) { + resolved.push(this.resolveDependency(identifier, wrapper)); + } + + wrapper.arguments = resolved; + } + + // Finally, call the wrapper's callback. + return wrapper.call(); + } + + /** + * Invokes the given callback and returns its output + * + * @param {Callback} callback + * @param {any[]} [args] + * + * @returns {any} + * + * @throws {ContainerError} + * + * @protected + */ + protected invokeCallback(callback: Callback, args: any[] = []): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + // When no arguments are provided, attempt to obtain and resolve dependencies + // for the callback. This will work if the callback is a class method with + // defined dependencies. Otherwise, this will not do anything... + if (args.length == 0 && this.hasDependencies(callback as object)) { + args = this.resolveDependencies(callback as object); + } + + return callback(...args); + } /** * Obtains and resolves dependencies for given target From 0cc1b00f6fbe32397a118903d420ae946f11d7c5 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 24 Mar 2024 19:10:28 +0100 Subject: [PATCH 119/224] Fix support/misc utils included in dist output --- packages/container/rollup.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/container/rollup.config.mjs b/packages/container/rollup.config.mjs index 9a9b0fa1..6df82b12 100644 --- a/packages/container/rollup.config.mjs +++ b/packages/container/rollup.config.mjs @@ -18,6 +18,7 @@ export default createConfig({ '@aedart/support/concerns', '@aedart/support/exceptions', '@aedart/support/meta', + '@aedart/support/misc', '@aedart/support/mixins', '@aedart/support/reflections', ] From 13b77eba5230621cd22d46890bad36b92221afcf Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 24 Mar 2024 19:11:33 +0100 Subject: [PATCH 120/224] Fix lint --- packages/container/src/BindingEntry.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/container/src/BindingEntry.ts b/packages/container/src/BindingEntry.ts index 7647fbb1..2d247769 100644 --- a/packages/container/src/BindingEntry.ts +++ b/packages/container/src/BindingEntry.ts @@ -10,7 +10,9 @@ import { isBindingIdentifier } from "@aedart/support/container"; /** * Binding Entry */ -export default class BindingEntry implements Binding +export default class BindingEntry< + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +> implements Binding { /** * This binding's identifier From d1df977212a3707dc777c757e69184ffd43f2ebf Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 24 Mar 2024 19:13:37 +0100 Subject: [PATCH 121/224] Fix lint --- packages/container/src/Container.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 1ca07b0f..b6413bd3 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -824,10 +824,10 @@ export default class Container implements ServiceContainerContract if (args.length == 0 && !wrapper.hasArguments() && hasAllMethods(wrapper, 'has', 'get') - /* @ts-ignore */ + /* @ts-expect-error TS7053 - has method is in wrapper at this point */ && wrapper['has'](DEPENDENCIES) ) { - /* @ts-ignore */ + /* @ts-expect-error TS7053 - get method is in wrapper at this point */ const dependencies: Identifier[] = wrapper['get']() ?? []; const resolved: any[] = []; /* eslint-disable-line @typescript-eslint/no-explicit-any */ for(const identifier of dependencies) { From 584c09ac727cc5d99f092099a8895edcad3cd448 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Sun, 24 Mar 2024 19:15:07 +0100 Subject: [PATCH 122/224] Fix symbol marked as "type" import --- packages/container/src/Container.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index b6413bd3..9f3faf9b 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -7,7 +7,6 @@ import type { FactoryCallback, Identifier, Binding, - DEPENDENCIES } from "@aedart/contracts/container"; import type { Callback, @@ -17,6 +16,7 @@ import type { ConstructorLike } from "@aedart/contracts"; import type { CallbackWrapper } from "@aedart/contracts/support"; +import { DEPENDENCIES } from "@aedart/contracts/container"; import { hasDependencies, getDependencies } from "@aedart/support/container"; import { getErrorMessage } from "@aedart/support/exceptions"; import { From 8c94203f1a6eb99288e02fad129187bea8573fdd Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 25 Mar 2024 10:56:02 +0100 Subject: [PATCH 123/224] Add "rebound" logic --- packages/container/src/Container.ts | 94 ++++++++++++++++++- packages/contracts/src/container/Container.ts | 13 +++ packages/contracts/src/container/types.ts | 9 ++ 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 9f3faf9b..68bc37bd 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -1,4 +1,4 @@ -import type { +import { AfterResolvedCallback, Alias, BeforeResolvedCallback, @@ -7,6 +7,7 @@ import type { FactoryCallback, Identifier, Binding, + ReboundCallback, } from "@aedart/contracts/container"; import type { Callback, @@ -109,10 +110,20 @@ export default class Container implements ServiceContainerContract * "After" resolved callbacks * * @type {Map} + * * @protected */ protected afterResolvedCallbacks: Map = new Map(); + /** + * Rebound callbacks + * + * @type {Map} + * + * @protected + */ + protected reboundCallbacks: Map = new Map(); + /** * Resolve stack * @@ -165,7 +176,11 @@ export default class Container implements ServiceContainerContract this.bindings.set(identifier, this.makeBindingEntry(identifier, concrete, shared)); - // TODO: isResolved() vs. rebound() ??? + // Invoke rebound callbacks, if the identifier has already been resolved, such that + // dependent objects can be updated... + if (this.isResolved(identifier)) { + this.rebound(identifier); + } return this; } @@ -238,9 +253,14 @@ export default class Container implements ServiceContainerContract */ public instance(identifier: Identifier, instance: T): T { + const isBound: boolean = this.has(identifier); + this.instances.set(identifier, instance as object); - // TODO: rebound() ??? + // If the identifier was already bound before, invoke the rebound callbacks. + if (isBound) { + this.rebound(identifier); + } return instance; } @@ -493,6 +513,8 @@ export default class Container implements ServiceContainerContract const instance = this.instances.get(identifier) as object; this.instances.set(identifier, callback(instance, this)); + + this.rebound(identifier); } else { // Otherwise, add extend callback to the existing for the identifier. if (!this.extenders.has(identifier)) { @@ -502,11 +524,42 @@ export default class Container implements ServiceContainerContract const existing: ExtendCallback[] = this.extenders.get(identifier) as ExtendCallback[]; existing.push(callback); this.extenders.set(identifier, existing); + + if (this.isResolved(identifier)) { + this.rebound(identifier); + } } return this; } + /** + * Register a callback to be invoked whenever identifier is "rebound" + * + * @param {Identifier} identifier + * @param {ReboundCallback} callback + * + * @return {any | void} + * + * @throws {ContainerException} + */ + public rebinding(identifier: Identifier, callback: ReboundCallback): any | void /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + identifier = this.getAlias(identifier); + + if (!this.reboundCallbacks.has(identifier)) { + this.reboundCallbacks.set(identifier, []); + } + + const existing: ReboundCallback[] = this.reboundCallbacks.get(identifier) as ReboundCallback[]; + existing.push(callback); + this.reboundCallbacks.set(identifier, existing); + + if (this.has(identifier)) { + return this.make(identifier); + } + } + /** * Forget binding and resolved instance for given identifier * @@ -915,6 +968,41 @@ export default class Container implements ServiceContainerContract throw new ContainerError(`Unable to resolve "${identifier.toString()}" for target "${getNameOrDesc(target as ConstructorLike)}": ${reason}`, options); } } + + /** + * Invokes "rebound" callbacks for given identifier + * + * @param {Identifier} identifier + * + * @return {void} + * + * @throws {ContainerError} + * + * @protected + */ + protected rebound(identifier: Identifier): void + { + const instance = this.make(identifier); + + const callbacks: ReboundCallback[] = this.getReboundCallbacks(identifier); + for (const callback of callbacks) { + callback(this, instance); + } + } + + /** + * Get "rebound" callbacks for given identifier + * + * @param {Identifier} identifier + * + * @return {ReboundCallback[]} + * + * @protected + */ + protected getReboundCallbacks(identifier: Identifier): ReboundCallback[] + { + return this.reboundCallbacks.get(identifier) ?? []; + } /** * Invokes the "before" resolved callbacks for given identifier diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index 26224ac9..9e58b193 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -9,6 +9,7 @@ import { Identifier, FactoryCallback, ExtendCallback, + ReboundCallback, BeforeResolvedCallback, AfterResolvedCallback, } from "./types"; @@ -229,6 +230,18 @@ export default interface Container * @throws {ContainerException} */ extend(identifier: Identifier, callback: ExtendCallback): this; + + /** + * Register a callback to be invoked whenever identifier is "rebound" + * + * @param {Identifier} identifier + * @param {ReboundCallback} callback + * + * @return {any | void} + * + * @throws {ContainerException} + */ + rebinding(identifier: Identifier, callback: ReboundCallback): any | void; /* eslint-disable-line @typescript-eslint/no-explicit-any */ /** * Forget binding and resolved instance for given identifier diff --git a/packages/contracts/src/container/types.ts b/packages/contracts/src/container/types.ts index c9df040a..87d0d004 100644 --- a/packages/contracts/src/container/types.ts +++ b/packages/contracts/src/container/types.ts @@ -37,6 +37,15 @@ export type ExtendCallback< ExtendedValue extends Value = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ > = (resolved: Value, container: Container) => ExtendedValue; +/** + * Rebound Callback + * + * Callback to be invoked when a binding is "rebound". + */ +export type ReboundCallback< + Instance = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +> = (container: Container, instance: Instance) => void; + /** * Before Resolved Callback * From 29a08f1c93e872f86bdde3b7c19df0cde800c279 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 25 Mar 2024 11:04:04 +0100 Subject: [PATCH 124/224] Add forgetInstance() --- packages/container/src/Container.ts | 20 +++++++++++++++---- packages/contracts/src/container/Container.ts | 9 +++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 68bc37bd..1bd3f1c0 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -569,15 +569,27 @@ export default class Container implements ServiceContainerContract */ public forget(identifier: Identifier): boolean { - const alias: Identifier = this.getAlias(identifier); - - const removedBinding: boolean = this.bindings.delete(alias); - const removedInstance: boolean = this.instances.delete(alias); const removedAlias: boolean = this.aliases.delete(identifier); + + identifier = this.getAlias(identifier); + const removedBinding: boolean = this.bindings.delete(identifier); + const removedInstance: boolean = this.instances.delete(identifier); return removedBinding || removedInstance || removedAlias; } + /** + * Forget resolved instance for given identifier + * + * @param {Identifier} identifier + * + * @return {boolean} + */ + public forgetInstance(identifier: Identifier): boolean + { + return this.instances.delete(this.getAlias(identifier)); + } + /** * Flush container of all bindings and resolved instances * diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index 9e58b193..b75ff480 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -251,6 +251,15 @@ export default interface Container * @returns {boolean} */ forget(identifier: Identifier): boolean; + + /** + * Forget resolved instance for given identifier + * + * @param {Identifier} identifier + * + * @return {boolean} + */ + forgetInstance(identifier: Identifier): boolean; /** * Flush container of all bindings and resolved instances From 434486102964d20daf346c2e951f1622834c3cac Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 25 Mar 2024 11:36:57 +0100 Subject: [PATCH 125/224] Add tests for bind() and singleton() methods --- .../support/container/container/bind.test.js | 60 ++++++++++++++++ .../container/container/singleton.test.js | 72 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 tests/browser/packages/support/container/container/bind.test.js create mode 100644 tests/browser/packages/support/container/container/singleton.test.js diff --git a/tests/browser/packages/support/container/container/bind.test.js b/tests/browser/packages/support/container/container/bind.test.js new file mode 100644 index 00000000..aac52a33 --- /dev/null +++ b/tests/browser/packages/support/container/container/bind.test.js @@ -0,0 +1,60 @@ +import { Container } from "@aedart/container"; + +describe('@aedart/support/container', () => { + describe('bind', () => { + + it('can bind factory callback', () => { + const container = new Container(); + + container.bind('alpha', () => 'beta'); + + // -------------------------------------------------------------------- // + + const result = container.make('alpha'); + expect(result) + .toBe('beta'); + }); + + it('"bind if" does not overwrite existing binding', () => { + const container = new Container(); + + container + .bindIf('alpha', () => 'beta') + .bindIf('alpha', () => 'gamma'); + + // -------------------------------------------------------------------- // + + const result = container.make('alpha'); + expect(result) + .toBe('beta'); + }); + + it('"bind if" registers when there is no existing binding', () => { + const container = new Container(); + + container + .bindIf('alpha', () => 'beta') + .bindIf('beta', () => 'gamma'); + + // -------------------------------------------------------------------- // + + const result = container.make('beta'); + expect(result) + .toBe('gamma'); + }); + + it('can bind constructor', () => { + class Bar {} + + const container = new Container(); + + container.bind('foo', Bar); + + // -------------------------------------------------------------------- // + + const result = container.make('foo'); + expect(result) + .toBeInstanceOf(Bar); + }); + }); +}); \ No newline at end of file diff --git a/tests/browser/packages/support/container/container/singleton.test.js b/tests/browser/packages/support/container/container/singleton.test.js new file mode 100644 index 00000000..0560e768 --- /dev/null +++ b/tests/browser/packages/support/container/container/singleton.test.js @@ -0,0 +1,72 @@ +import { Container } from "@aedart/container"; + +describe('@aedart/support/container', () => { + describe('singleton', () => { + + it('can register "shared" binding', () => { + class A {} + + const container = new Container(); + + container.singleton('a', () => new A()); + + // -------------------------------------------------------------------- // + + const first = container.make('a'); + const second = container.make('a'); + + expect(first) + .toBe(second); + }); + + it('"singleton if" does not overwrite existing binding', () => { + class A {} + class B {} + + const container = new Container(); + + container + .singletonIf('a', () => new A()) + .singletonIf('a', () => new B()); + + // -------------------------------------------------------------------- // + + const result = container.make('a'); + expect(result) + .toBeInstanceOf(A); + }); + + it('"singleton if" registers when there is no existing binding', () => { + class A {} + class B {} + + const container = new Container(); + + container + .singletonIf('a', () => new A()) + .singletonIf('b', () => new B()); + + // -------------------------------------------------------------------- // + + const result = container.make('b'); + expect(result) + .toBeInstanceOf(B); + }); + + it('can register constructor as "shared" binding', () => { + class Bar {} + + const container = new Container(); + + container.singleton('foo', Bar); + + // -------------------------------------------------------------------- // + + const first = container.make('foo'); + const second = container.make('foo'); + + expect(first) + .toBe(second); + }); + }); +}); \ No newline at end of file From 5c3e58e318e87f7ae92f66aebb8751bcc21a1da5 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 25 Mar 2024 13:09:49 +0100 Subject: [PATCH 126/224] Add tests for instance() --- .../container/container/instance.test.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/browser/packages/support/container/container/instance.test.js diff --git a/tests/browser/packages/support/container/container/instance.test.js b/tests/browser/packages/support/container/container/instance.test.js new file mode 100644 index 00000000..896bac15 --- /dev/null +++ b/tests/browser/packages/support/container/container/instance.test.js @@ -0,0 +1,25 @@ +import { Container } from "@aedart/container"; + +describe('@aedart/support/container', () => { + describe('instance', () => { + + it('can register existing instance', () => { + class A {} + + const container = new Container(); + + const instance = container.instance('a', new A()); + + // -------------------------------------------------------------------- // + + const result = container.get('a'); + + expect(container.bound('a')) + .withContext('Instance does not appear to be bound') + .toBeTrue(); + + expect(result) + .toBe(instance); + }); + }); +}); \ No newline at end of file From c2e3d8fe477e1567ccab9606615b9bd1cd1b02bd Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 25 Mar 2024 13:10:12 +0100 Subject: [PATCH 127/224] Change test, add is "bound" expectation --- .../browser/packages/support/container/container/bind.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/browser/packages/support/container/container/bind.test.js b/tests/browser/packages/support/container/container/bind.test.js index aac52a33..eb238629 100644 --- a/tests/browser/packages/support/container/container/bind.test.js +++ b/tests/browser/packages/support/container/container/bind.test.js @@ -9,6 +9,10 @@ describe('@aedart/support/container', () => { container.bind('alpha', () => 'beta'); // -------------------------------------------------------------------- // + + expect(container.bound('alpha')) + .withContext('Binding not registered in container') + .toBeTrue(); const result = container.make('alpha'); expect(result) From 7c31cb9857fd8044de8199d2b26dceb80f199eb1 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 25 Mar 2024 13:13:01 +0100 Subject: [PATCH 128/224] Change tests, add isShared() expectations --- .../packages/support/container/container/instance.test.js | 4 ++++ .../packages/support/container/container/singleton.test.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/tests/browser/packages/support/container/container/instance.test.js b/tests/browser/packages/support/container/container/instance.test.js index 896bac15..da960022 100644 --- a/tests/browser/packages/support/container/container/instance.test.js +++ b/tests/browser/packages/support/container/container/instance.test.js @@ -17,6 +17,10 @@ describe('@aedart/support/container', () => { expect(container.bound('a')) .withContext('Instance does not appear to be bound') .toBeTrue(); + + expect(container.isShared('a')) + .withContext('Instance is not marked as "shared"') + .toBeTrue(); expect(result) .toBe(instance); diff --git a/tests/browser/packages/support/container/container/singleton.test.js b/tests/browser/packages/support/container/container/singleton.test.js index 0560e768..c250b4c0 100644 --- a/tests/browser/packages/support/container/container/singleton.test.js +++ b/tests/browser/packages/support/container/container/singleton.test.js @@ -12,6 +12,10 @@ describe('@aedart/support/container', () => { // -------------------------------------------------------------------- // + expect(container.isShared('a')) + .withContext('Binding is not marked as "shared"') + .toBeTrue(); + const first = container.make('a'); const second = container.make('a'); From 99e01194a61de5d86ae3b48237a13485c3112f15 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 25 Mar 2024 13:24:01 +0100 Subject: [PATCH 129/224] Fix semantics --- packages/container/src/Container.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 1bd3f1c0..ec1683d9 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -357,17 +357,17 @@ export default class Container implements ServiceContainerContract } /** - * Returns the alias for given identifier, if available + * Returns the identifier for given alias, if available * - * @param {Identifier} identifier + * @param {Identifier} alias * * @returns {Identifier} */ - public getAlias(identifier: Identifier): Identifier + public getAlias(alias: Identifier): Identifier { - return this.aliases.has(identifier) - ? this.getAlias(this.aliases.get(identifier) as Identifier) - : identifier; + return this.aliases.has(alias) + ? this.getAlias(this.aliases.get(alias) as Identifier) + : alias; } /** From 850cd36d8c38845c935dfc08fdb652ed930ca477 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 25 Mar 2024 13:28:14 +0100 Subject: [PATCH 130/224] Add tests for alias() and alias related utils --- .../support/container/container/alias.test.js | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/browser/packages/support/container/container/alias.test.js diff --git a/tests/browser/packages/support/container/container/alias.test.js b/tests/browser/packages/support/container/container/alias.test.js new file mode 100644 index 00000000..bfd89ee5 --- /dev/null +++ b/tests/browser/packages/support/container/container/alias.test.js @@ -0,0 +1,51 @@ +import { Container } from "@aedart/container"; + +describe('@aedart/support/container', () => { + describe('alias', () => { + + it('can register alias for binding', () => { + const container = new Container(); + + container + .bind('alpha', () => 'beta') + .alias('alpha', 'beta'); + + // -------------------------------------------------------------------- // + + expect(container.isAlias('beta')) + .withContext('Alias not registered') + .toBeTrue(); + + expect(container.bound('beta')) + .withContext('Alias not bound') + .toBeTrue(); + + const result = container.make('beta'); + expect(result) + .toBe('beta'); + }); + + it('fails when identifier is the same as the alias', () => { + const container = new Container(); + + const callback = () => { + container.alias('a', 'a'); + } + + expect(callback) + .toThrowError(TypeError); + }); + + it('can obtain identifier for alias', () => { + const container = new Container(); + + container.alias('alpha', 'beta'); + + // -------------------------------------------------------------------- // + + const result = container.getAlias('beta'); + expect(result) + .toBe('alpha'); + }); + }); +}); \ No newline at end of file From ebda22efacb319c023b7aebf21123b0aed9bd968 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 26 Mar 2024 12:59:52 +0100 Subject: [PATCH 131/224] Add tests for make() --- .../support/container/container/make.test.js | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/browser/packages/support/container/container/make.test.js diff --git a/tests/browser/packages/support/container/container/make.test.js b/tests/browser/packages/support/container/container/make.test.js new file mode 100644 index 00000000..e2caaf8c --- /dev/null +++ b/tests/browser/packages/support/container/container/make.test.js @@ -0,0 +1,71 @@ +import { Container, NotFoundError } from "@aedart/container"; + +describe('@aedart/support/container', () => { + describe('make', () => { + + it('passes container instance to factory callback', () => { + const container = new Container(); + + container.bind('a', (c) => c); + + // -------------------------------------------------------------------- // + + const result = container.make('a'); + + expect(result) + .toBe(container); + }); + + it('resolves with provided arguments', () => { + const container = new Container(); + + class A { + name; + + constructor(name) { + this.name = name; + } + } + + const identifier = Symbol('my_instance'); + + container.bind(identifier, (container, ...args) => { + return new A(...args); + }); + + // -------------------------------------------------------------------- // + + const name = 'Alfred'; + const result = container.make(identifier, [ name ]); + + expect(result) + .toBeInstanceOf(A); + expect(result.name) + .toBe(name); + }); + + it('resolves buildable identifier (constructor), when binding not registered', () => { + const container = new Container(); + + class Foo {} + + // -------------------------------------------------------------------- // + + const result = container.make(Foo); + + expect(result) + .toBeInstanceOf(Foo); + }); + + it('fails when binding does not exist, and not buildable', () => { + const container = new Container(); + + const callback = () => { + container.make('my_service'); + } + + expect(callback) + .toThrowError(NotFoundError); + }); + }); +}); \ No newline at end of file From d782a676ad145f6c8a44334f1756590aaf11821b Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 26 Mar 2024 13:02:30 +0100 Subject: [PATCH 132/224] Change order of callback arguments, when callback given as defaultValue --- packages/container/src/Container.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index ec1683d9..57cee220 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -411,7 +411,7 @@ export default class Container implements ServiceContainerContract { if (!this.has(identifier) && !this.isBuildable(identifier)) { if (typeof defaultValue === 'function') { - return defaultValue(args, this); + return defaultValue(this, args); } return defaultValue as D; From cdf5822a2d9089c0fb65db21c1c2b8f671ca905b Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 26 Mar 2024 13:13:38 +0100 Subject: [PATCH 133/224] Add tests for makeOrDefault() --- .../support/container/container/make.test.js | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/browser/packages/support/container/container/make.test.js b/tests/browser/packages/support/container/container/make.test.js index e2caaf8c..2c0df886 100644 --- a/tests/browser/packages/support/container/container/make.test.js +++ b/tests/browser/packages/support/container/container/make.test.js @@ -68,4 +68,68 @@ describe('@aedart/support/container', () => { .toThrowError(NotFoundError); }); }); + + describe('makeOrDefault', () => { + + it('resolves when binding exists', () => { + const container = new Container(); + + container.bind('a', () => 'b'); + + // -------------------------------------------------------------------- // + + const result = container.makeOrDefault('a'); + + expect(result) + .toBe('b'); + }); + + it('resolves buildable identifier, when binding does not exist', () => { + const container = new Container(); + + class Bar {} + + // -------------------------------------------------------------------- // + + const result = container.makeOrDefault(Bar); + + expect(result) + .toBeInstanceOf(Bar); + }); + + it('returns default value, when binding does not exist', () => { + const container = new Container(); + + // -------------------------------------------------------------------- // + + const result = container.makeOrDefault('api', [], 'default'); + + expect(result) + .toBe('default'); + }); + + it('invokes callback when given as default value', () => { + const container = new Container(); + + class Service { + name; + + constructor(name) { + this.name = name; + } + } + + // -------------------------------------------------------------------- // + + const name = 'My Service'; + const result = container.makeOrDefault('api', [ name ], (c, args) => { + return new Service(...args) + }); + + expect(result) + .toBeInstanceOf(Service); + expect(result.name) + .toBe(name); + }); + }); }); \ No newline at end of file From c573920ea6bb91115a904c4e6fc5869a962eeb68 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 26 Mar 2024 17:14:24 +0100 Subject: [PATCH 134/224] Move tests to correct "container" suite --- .../packages/{support => }/container/container/alias.test.js | 0 .../packages/{support => }/container/container/bind.test.js | 0 .../{support => }/container/container/getInstance.test.js | 0 .../packages/{support => }/container/container/instance.test.js | 0 .../packages/{support => }/container/container/make.test.js | 0 .../packages/{support => }/container/container/singleton.test.js | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename tests/browser/packages/{support => }/container/container/alias.test.js (100%) rename tests/browser/packages/{support => }/container/container/bind.test.js (100%) rename tests/browser/packages/{support => }/container/container/getInstance.test.js (100%) rename tests/browser/packages/{support => }/container/container/instance.test.js (100%) rename tests/browser/packages/{support => }/container/container/make.test.js (100%) rename tests/browser/packages/{support => }/container/container/singleton.test.js (100%) diff --git a/tests/browser/packages/support/container/container/alias.test.js b/tests/browser/packages/container/container/alias.test.js similarity index 100% rename from tests/browser/packages/support/container/container/alias.test.js rename to tests/browser/packages/container/container/alias.test.js diff --git a/tests/browser/packages/support/container/container/bind.test.js b/tests/browser/packages/container/container/bind.test.js similarity index 100% rename from tests/browser/packages/support/container/container/bind.test.js rename to tests/browser/packages/container/container/bind.test.js diff --git a/tests/browser/packages/support/container/container/getInstance.test.js b/tests/browser/packages/container/container/getInstance.test.js similarity index 100% rename from tests/browser/packages/support/container/container/getInstance.test.js rename to tests/browser/packages/container/container/getInstance.test.js diff --git a/tests/browser/packages/support/container/container/instance.test.js b/tests/browser/packages/container/container/instance.test.js similarity index 100% rename from tests/browser/packages/support/container/container/instance.test.js rename to tests/browser/packages/container/container/instance.test.js diff --git a/tests/browser/packages/support/container/container/make.test.js b/tests/browser/packages/container/container/make.test.js similarity index 100% rename from tests/browser/packages/support/container/container/make.test.js rename to tests/browser/packages/container/container/make.test.js diff --git a/tests/browser/packages/support/container/container/singleton.test.js b/tests/browser/packages/container/container/singleton.test.js similarity index 100% rename from tests/browser/packages/support/container/container/singleton.test.js rename to tests/browser/packages/container/container/singleton.test.js From 22e78e27a6b2fc29926faddfecee5d7ba182e10b Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 26 Mar 2024 17:24:40 +0100 Subject: [PATCH 135/224] Improve thrown error when not buildable --- packages/container/src/Container.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 57cee220..0e2998f8 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -453,7 +453,8 @@ export default class Container implements ServiceContainerContract // Abort if concrete is not buildable if (!this.isBuildable(concrete)) { - throw new ContainerError(`Unable to build concrete instance`, { cause: { concrete: concrete, args: args } }); + const nameOrDesc: string = getNameOrDesc(concrete as ConstructorLike); + throw new ContainerError(`Unable to build concrete: ${nameOrDesc} is not a constructor or a Binding Entry`, { cause: { concrete: concrete, args: args } }); } // Resolve the constructor and eventual dependencies, when no arguments are given. From 40dfa48cb22591fce2c86bd9202bfa7b2469d64a Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 26 Mar 2024 19:03:15 +0100 Subject: [PATCH 136/224] Fix error not configured --- packages/container/src/exceptions/NotFoundError.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/container/src/exceptions/NotFoundError.ts b/packages/container/src/exceptions/NotFoundError.ts index 4ea9c3ec..edf4c86d 100644 --- a/packages/container/src/exceptions/NotFoundError.ts +++ b/packages/container/src/exceptions/NotFoundError.ts @@ -1,5 +1,6 @@ import type { NotFoundException } from "@aedart/contracts/container"; import ContainerError from "./ContainerError"; +import { configureCustomError } from "@aedart/support/exceptions"; /** * Not Found Error @@ -17,5 +18,7 @@ export default class NotFoundError extends ContainerError implements NotFoundExc constructor(message?: string, options?: ErrorOptions) { super(message, options); + + configureCustomError(this); } } \ No newline at end of file From 74404cad2b5b050cc4af7b28945e7ccbe27bd675 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 26 Mar 2024 19:04:38 +0100 Subject: [PATCH 137/224] Improve circular dependency error handling Now throwing a specific exception, and allowing it to bubble upwards to improve the shown error message. This should make it much easier to understand what is going on. --- packages/container/src/Container.ts | 28 +++++++++++++++++-- .../src/exceptions/CircularDependencyError.ts | 22 +++++++++++++++ packages/container/src/exceptions/index.ts | 2 ++ .../exceptions/CircularDependencyException.ts | 8 ++++++ .../src/container/exceptions/index.ts | 2 ++ 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 packages/container/src/exceptions/CircularDependencyError.ts create mode 100644 packages/contracts/src/container/exceptions/CircularDependencyException.ts diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 0e2998f8..9d913fba 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -27,6 +27,7 @@ import { hasAllMethods, } from "@aedart/support/reflections"; import { isCallbackWrapper } from "@aedart/support"; +import CircularDependencyError from "./exceptions/CircularDependencyError"; import ContainerError from "./exceptions/ContainerError"; import NotFoundError from "./exceptions/NotFoundError"; import BindingEntry from "./BindingEntry"; @@ -820,6 +821,21 @@ export default class Container implements ServiceContainerContract } catch (e) { identifier = identifier ?? target; + if (e instanceof CircularDependencyError) { + if (e.cause === undefined) { + e.cause = Object.create(null); + } + + (e.cause as any).target = target; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + (e.cause as any).args = args; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + (e.cause as any).identifier = identifier; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + (e.cause as any).resolveStack = Array.from(this.resolveStack); /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + this.resolveStack.delete(target as Constructor); + + throw e; + } + const reason: string = getErrorMessage(e); const options = { cause: { @@ -970,6 +986,10 @@ export default class Container implements ServiceContainerContract try { return this.make(identifier); } catch (e) { + if (e instanceof CircularDependencyError) { + throw e; + } + const reason: string = getErrorMessage(e); const options = { cause: { @@ -1160,7 +1180,7 @@ export default class Container implements ServiceContainerContract * * @param {Constructor} target * - * @throws {ContainerError} + * @throws {CircularDependencyError} * * @protected */ @@ -1179,9 +1199,11 @@ export default class Container implements ServiceContainerContract // to understand how we got here... const stack: string[] = Array.from(this.resolveStack).map((ctor: Constructor) => { return getNameOrDesc(ctor); - }) + }); + stack.push(getNameOrDesc(target)); + const trace: string = stack.join(' -> '); - throw new ContainerError(`Circular Dependency for target "${getNameOrDesc(target as ConstructorLike)}": ${trace}`); + throw new CircularDependencyError(`Circular Dependency for target "${getNameOrDesc(target as ConstructorLike)}". Resolve stack: ${trace}`); } } \ No newline at end of file diff --git a/packages/container/src/exceptions/CircularDependencyError.ts b/packages/container/src/exceptions/CircularDependencyError.ts new file mode 100644 index 00000000..aed50643 --- /dev/null +++ b/packages/container/src/exceptions/CircularDependencyError.ts @@ -0,0 +1,22 @@ +import type { CircularDependencyException } from "@aedart/contracts/container"; +import ContainerError from "./ContainerError"; +import { configureCustomError } from "@aedart/support/exceptions"; + +/** + * Circular Dependency Error + */ +export default class CircularDependencyError extends ContainerError implements CircularDependencyException +{ + /** + * Create new Not Found Error instance + * + * @param {string} [message] + * @param {ErrorOptions} [options] + */ + constructor(message?: string, options?: ErrorOptions) + { + super(message, options); + + configureCustomError(this); + } +} \ No newline at end of file diff --git a/packages/container/src/exceptions/index.ts b/packages/container/src/exceptions/index.ts index 73ee0de1..db9803db 100644 --- a/packages/container/src/exceptions/index.ts +++ b/packages/container/src/exceptions/index.ts @@ -1,6 +1,8 @@ +import CircularDependencyError from "./CircularDependencyError"; import ContainerError from "./ContainerError"; import NotFoundError from "./NotFoundError"; export { + CircularDependencyError, ContainerError, NotFoundError } \ No newline at end of file diff --git a/packages/contracts/src/container/exceptions/CircularDependencyException.ts b/packages/contracts/src/container/exceptions/CircularDependencyException.ts new file mode 100644 index 00000000..58a5b93f --- /dev/null +++ b/packages/contracts/src/container/exceptions/CircularDependencyException.ts @@ -0,0 +1,8 @@ +import type ContainerException from "./ContainerException"; + +/** + * Circular Dependency Exception + * + * To be thrown in situations when a binding has a circular dependency. + */ +export default interface CircularDependencyException extends ContainerException {} \ No newline at end of file diff --git a/packages/contracts/src/container/exceptions/index.ts b/packages/contracts/src/container/exceptions/index.ts index 2d5af743..dcc19dd3 100644 --- a/packages/contracts/src/container/exceptions/index.ts +++ b/packages/contracts/src/container/exceptions/index.ts @@ -1,6 +1,8 @@ +import CircularDependencyException from "./CircularDependencyException"; import ContainerException from "./ContainerException"; import NotFoundException from "./NotFoundException"; export { + type CircularDependencyException, type ContainerException, type NotFoundException } \ No newline at end of file From 21bba2c5f63e5b8e9ba68f54220734dde5b40870 Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 26 Mar 2024 19:11:34 +0100 Subject: [PATCH 138/224] Set target and identifier, when circular dependency error thrown --- packages/container/src/Container.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 9d913fba..daf4b655 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -987,6 +987,13 @@ export default class Container implements ServiceContainerContract return this.make(identifier); } catch (e) { if (e instanceof CircularDependencyError) { + if (e.cause === undefined) { + e.cause = Object.create(null); + } + + (e.cause as any).target = target; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + (e.cause as any).identifier = identifier; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + throw e; } From 56f958d08dadf78d5edcd990b4625714eaa28c2c Mon Sep 17 00:00:00 2001 From: Alin Eugen Deac Date: Tue, 26 Mar 2024 19:11:43 +0100 Subject: [PATCH 139/224] Add tests for build() --- .../container/container/build.test.js | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 tests/browser/packages/container/container/build.test.js diff --git a/tests/browser/packages/container/container/build.test.js b/tests/browser/packages/container/container/build.test.js new file mode 100644 index 00000000..a34dca79 --- /dev/null +++ b/tests/browser/packages/container/container/build.test.js @@ -0,0 +1,200 @@ +import {Container, BindingEntry, ContainerError, CircularDependencyError } from "@aedart/container"; +import { dependencies } from "@aedart/support/container"; + +describe('@aedart/support/container', () => { + describe('build', () => { + + it('fails when concrete is not buildable', () => { + const container = new Container(); + + const callback = () => { + container.build('not_buildable'); + } + + expect(callback) + .toThrowError(ContainerError); + }); + + it('can build constructor type', () => { + const container = new Container(); + + class Service {} + + // -------------------------------------------------------------------- // + + const result = container.build(Service); + + expect(result) + .toBeInstanceOf(Service); + }); + + it('can build binding entry type', () => { + const container = new Container(); + + class Service {} + + const binding = new BindingEntry('my_service', Service); + + // -------------------------------------------------------------------- // + + const result = container.build(binding); + + expect(result) + .toBeInstanceOf(Service); + }); + + it('passes arguments to factory callback', () => { + const container = new Container(); + + class Cache { + ttl; + + constructor(ttl) { + this.ttl = ttl; + } + } + + const binding = new BindingEntry('my_service', (container, ...args) => { + return new Cache(...args); + }); + + // -------------------------------------------------------------------- // + + const ttl = 500; + const result = container.build(binding, [ ttl ]); + + expect(result) + .toBeInstanceOf(Cache); + expect(result.ttl) + .toBe(ttl); + }); + + it('passes arguments to constructor', () => { + const container = new Container(); + + class Cache { + ttl; + + constructor(ttl) { + this.ttl = ttl; + } + } + + const binding = new BindingEntry('my_service', Cache); + + // -------------------------------------------------------------------- // + + const ttl = 200; + const result = container.build(binding, [ ttl ]); + + expect(result) + .toBeInstanceOf(Cache); + expect(result.ttl) + .toBe(ttl); + }); + + it('resolves dependencies for constructor', () => { + const container = new Container(); + + class MyDriver {} + + @dependencies('my_driver') + class Service { + driver; + + constructor(driver) { + this.driver = driver; + } + } + + container.bind('my_driver', MyDriver); + + // -------------------------------------------------------------------- // + + const result = container.build(Service); + + expect(result.driver) + .toBeInstanceOf(MyDriver); + }); + + it('resolves nested dependencies ', () => { + const container = new Container(); + + class A {} + + @dependencies('a') + class B { + item; + + constructor(item) { + this.item = item; + } + } + + @dependencies('b') + class C { + item; + + constructor(item) { + this.item = item; + } + } + + container + .bind('a', A) + .bind('b', B); + + // -------------------------------------------------------------------- // + + const result = container.build(C); + + expect(result.item) + .withContext('B was expected as resolved dependency') + .toBeInstanceOf(B); + + expect(result.item.item) + .withContext('A was expected as nested resolved dependency') + .toBeInstanceOf(A); + }); + + it('fails on circular dependency', () => { + const container = new Container(); + + // NOTE: This causes a circular dependency when resolving... + @dependencies('c') + class A {} + + @dependencies('a') + class B { + item; + + constructor(item) { + this.item = item; + } + } + + @dependencies('b') + class C { + item; + + constructor(item) { + this.item = item; + } + } + + container + .bind('a', A) + .bind('b', B) + .bind('c', C); + + // -------------------------------------------------------------------- // + + const callback = () => { + return container.build(C); + } + + expect(callback) + .toThrowError(CircularDependencyError); + }); + }); +}); \ No newline at end of file From 3576c64c1a2651b78fba64e1b37d7f4c2cd999aa Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 27 Mar 2024 13:38:07 +0100 Subject: [PATCH 140/224] Fix missing JSDoc @type for callback options --- packages/contracts/src/support/objects/merge/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contracts/src/support/objects/merge/types.ts b/packages/contracts/src/support/objects/merge/types.ts index 462fddb3..d40dc9da 100644 --- a/packages/contracts/src/support/objects/merge/types.ts +++ b/packages/contracts/src/support/objects/merge/types.ts @@ -60,6 +60,8 @@ export type MergeCallback = ( /** * The merge options to be applied + * + * @type {Readonly} */ options: Readonly ) => any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ From 23aff6e1f7bfb311afcb1d68f6c543233cf43c2c Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 27 Mar 2024 13:57:12 +0100 Subject: [PATCH 141/224] Refactor / Redesign array merge, use a custom "Merger" class Now the array merge has a similar structure and behaviour as the object merge. --- .../contracts/src/support/arrays/index.ts | 4 +- .../arrays/merge/ArrayMergeException.ts | 8 + .../support/arrays/merge/ArrayMergeOptions.ts | 29 +++ .../src/support/arrays/merge/ArrayMerger.ts | 167 ++++++++++++++++ .../src/support/arrays/merge/index.ts | 11 ++ .../src/support/arrays/merge/types.ts | 34 ++++ .../src/arrays/exceptions/ArrayMergeError.ts | 4 +- packages/support/src/arrays/index.ts | 1 + packages/support/src/arrays/merge.ts | 181 +++++++++++++++--- .../support/src/arrays/merge/ArrayMerger.ts | 109 +++++++++++ .../arrays/merge/DefaultArrayMergeOptions.ts | 63 ++++++ .../arrays/merge/defaultArrayMergeCallback.ts | 27 +++ packages/support/src/arrays/merge/index.ts | 8 + 13 files changed, 619 insertions(+), 27 deletions(-) create mode 100644 packages/contracts/src/support/arrays/merge/ArrayMergeException.ts create mode 100644 packages/contracts/src/support/arrays/merge/ArrayMergeOptions.ts create mode 100644 packages/contracts/src/support/arrays/merge/ArrayMerger.ts create mode 100644 packages/contracts/src/support/arrays/merge/index.ts create mode 100644 packages/contracts/src/support/arrays/merge/types.ts create mode 100644 packages/support/src/arrays/merge/ArrayMerger.ts create mode 100644 packages/support/src/arrays/merge/DefaultArrayMergeOptions.ts create mode 100644 packages/support/src/arrays/merge/defaultArrayMergeCallback.ts create mode 100644 packages/support/src/arrays/merge/index.ts diff --git a/packages/contracts/src/support/arrays/index.ts b/packages/contracts/src/support/arrays/index.ts index 09aac224..d8154922 100644 --- a/packages/contracts/src/support/arrays/index.ts +++ b/packages/contracts/src/support/arrays/index.ts @@ -8,4 +8,6 @@ export const SUPPORT_ARRAYS: unique symbol = Symbol('@aedart/contracts/support/a import ConcatSpreadable from "./ConcatSpreadable"; export { type ConcatSpreadable -} \ No newline at end of file +} + +export * from './merge' \ No newline at end of file diff --git a/packages/contracts/src/support/arrays/merge/ArrayMergeException.ts b/packages/contracts/src/support/arrays/merge/ArrayMergeException.ts new file mode 100644 index 00000000..c47231d8 --- /dev/null +++ b/packages/contracts/src/support/arrays/merge/ArrayMergeException.ts @@ -0,0 +1,8 @@ +import type { Throwable } from "@aedart/contracts/support/exceptions"; + +/** + * Array Merge Exception + * + * To be thrown when unable to merge arrays. + */ +export default interface ArrayMergeException extends Throwable {} \ No newline at end of file diff --git a/packages/contracts/src/support/arrays/merge/ArrayMergeOptions.ts b/packages/contracts/src/support/arrays/merge/ArrayMergeOptions.ts new file mode 100644 index 00000000..78f623c0 --- /dev/null +++ b/packages/contracts/src/support/arrays/merge/ArrayMergeOptions.ts @@ -0,0 +1,29 @@ +import { ArrayMergeCallback } from "./types"; + +/** + * Array Merge Options + */ +export default interface ArrayMergeOptions +{ + /** + * Transfer functions + * + * **When `true`**: _functions are transferred into resulting array._ + * + * **When `false` (_default behaviour_)**: _The merge operation will fail when a function + * is encountered (functions are not cloneable by default)._ + * + * @type {boolean} + */ + transferFunctions?: boolean; + + /** + * Merge callback to be applied + * + * **Note**: _When no callback is provided, then the merge function's default + * callback is used._ + * + * @type {ArrayMergeCallback} + */ + callback?: ArrayMergeCallback; +} \ No newline at end of file diff --git a/packages/contracts/src/support/arrays/merge/ArrayMerger.ts b/packages/contracts/src/support/arrays/merge/ArrayMerger.ts new file mode 100644 index 00000000..21e6bb97 --- /dev/null +++ b/packages/contracts/src/support/arrays/merge/ArrayMerger.ts @@ -0,0 +1,167 @@ +import ArrayMergeOptions from "./ArrayMergeOptions"; +import { ArrayMergeCallback } from "./types"; + +/** + * Array Merger + * + * Able to merge (deep merge) multiple source arrays into a single new array. + */ +export default interface ArrayMerger +{ + /** + * Use the following merge options or callback + * + * @param {ArrayMergeCallback | ArrayMergeOptions} [options] + * + * @return {this} + * + * @throws {ArrayMergeException} + */ + using(options?: ArrayMergeCallback | ArrayMergeOptions): this; + + /** + * Returns a merger of given source arrays + * + * @template SourceA extends any[] + * + * @param {SourceA} a + * + * @returns {SourceA} + * + * @throws {ArrayMergeException} + */ + of< + SourceA extends any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(a: SourceA): SourceA; + + /** + * Returns a merger of given source arrays + * + * @template SourceA extends any[] + * @template SourceB extends any[] + * + * @param {SourceA} a + * @param {SourceB} b + * + * @returns {SourceA & SourceB} + * + * @throws {ArrayMergeException} + */ + of< + SourceA extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceB extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(a: SourceA, b: SourceB): SourceA & SourceB; + + /** + * Returns a merger of given source arrays + * + * @template SourceA extends any[] + * @template SourceB extends any[] + * @template SourceC extends any[] + * + * @param {SourceA} a + * @param {SourceB} b + * @param {SourceC} c + * + * @returns {SourceA & SourceB & SourceC} + * + * @throws {ArrayMergeException} + */ + of< + SourceA extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceB extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceC extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(a: SourceA, b: SourceB, c: SourceC): SourceA & SourceB & SourceC; + + /** + * Returns a merger of given source arrays + * + * @template SourceA extends any[] + * @template SourceB extends any[] + * @template SourceC extends any[] + * @template SourceD extends any[] + * + * @param {SourceA} a + * @param {SourceB} b + * @param {SourceC} c + * @param {SourceD} d + * + * @returns {SourceA & SourceB & SourceC & SourceD} + * + * @throws {ArrayMergeException} + */ + of< + SourceA extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceB extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceC extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceD extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(a: SourceA, b: SourceB, c: SourceC, d: SourceD): SourceA & SourceB & SourceC & SourceD; + + /** + * Returns a merger of given source arrays + * + * @template SourceA extends any[] + * @template SourceB extends any[] + * @template SourceC extends any[] + * @template SourceD extends any[] + * @template SourceE extends any[] + * + * @param {SourceA} a + * @param {SourceB} b + * @param {SourceC} c + * @param {SourceD} d + * @param {SourceE} e + * + * @returns {SourceA & SourceB & SourceC & SourceD & SourceE} + * + * @throws {ArrayMergeException} + */ + of< + SourceA extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceB extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceC extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceD extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceE extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(a: SourceA, b: SourceB, c: SourceC, d: SourceD, e: SourceE): SourceA & SourceB & SourceC & SourceD & SourceE; + + /** + * Returns a merger of given source arrays + * + * @template SourceA extends any[] + * @template SourceB extends any[] + * @template SourceC extends any[] + * @template SourceD extends any[] + * @template SourceE extends any[] + * @template SourceF extends any[] + * + * @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 {ArrayMergeException} + */ + of< + SourceA extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceB extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceC extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceD extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceE extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceF extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(a: SourceA, b: SourceB, c: SourceC, d: SourceD, e: SourceE, f: SourceF): SourceA & SourceB & SourceC & SourceD & SourceE & SourceF; + + /** + * Returns a merger of given source arrays + * + * @param {...any[]} sources + * + * @return {any[]} + * + * @throws {ArrayMergeException} + */ + of(...sources: any[]): any[]; /* eslint-disable-line @typescript-eslint/no-explicit-any */ +} \ No newline at end of file diff --git a/packages/contracts/src/support/arrays/merge/index.ts b/packages/contracts/src/support/arrays/merge/index.ts new file mode 100644 index 00000000..794fa70a --- /dev/null +++ b/packages/contracts/src/support/arrays/merge/index.ts @@ -0,0 +1,11 @@ +import ArrayMergeException from "./ArrayMergeException"; +import ArrayMergeOptions from "./ArrayMergeOptions"; +import ArrayMerger from "./ArrayMerger"; + +export { + type ArrayMergeException, + type ArrayMergeOptions, + type ArrayMerger +} + +export * from './types'; \ No newline at end of file diff --git a/packages/contracts/src/support/arrays/merge/types.ts b/packages/contracts/src/support/arrays/merge/types.ts new file mode 100644 index 00000000..fe5308ee --- /dev/null +++ b/packages/contracts/src/support/arrays/merge/types.ts @@ -0,0 +1,34 @@ +import ArrayMergeOptions from "./ArrayMergeOptions"; + +/** + * Array Merge Callback + */ +export type ArrayMergeCallback = ( + /** + * The current element being processed in the array + * + * @type {any} + */ + element: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + /** + * The index of the current element being processed in the array. + * + * @type {number} + */ + index: number, + + /** + * The concatenated array this callback was called upon + * + * @type {any[]} + */ + array: any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + /** + * The merge options to be applied + * + * @type {Readonly} + */ + options: Readonly +) => any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ \ No newline at end of file diff --git a/packages/support/src/arrays/exceptions/ArrayMergeError.ts b/packages/support/src/arrays/exceptions/ArrayMergeError.ts index 30bb962c..fe05c366 100644 --- a/packages/support/src/arrays/exceptions/ArrayMergeError.ts +++ b/packages/support/src/arrays/exceptions/ArrayMergeError.ts @@ -1,4 +1,4 @@ -import type { Throwable } from "@aedart/contracts/support/exceptions"; +import type { ArrayMergeException } from "@aedart/contracts/support/arrays"; import { configureCustomError } from "@aedart/support/exceptions"; /** @@ -6,7 +6,7 @@ import { configureCustomError } from "@aedart/support/exceptions"; * * To be thrown when two or more arrays are unable to be merged. */ -export default class ArrayMergeError extends Error implements Throwable +export default class ArrayMergeError extends Error implements ArrayMergeException { /** * Create a new Array Merge Error instance diff --git a/packages/support/src/arrays/index.ts b/packages/support/src/arrays/index.ts index 5f3aed30..984a2c1f 100644 --- a/packages/support/src/arrays/index.ts +++ b/packages/support/src/arrays/index.ts @@ -6,4 +6,5 @@ export * from './isSafeArrayLike'; export * from './isTypedArray'; export * from './merge'; +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 index d0dea0ab..06401562 100644 --- a/packages/support/src/arrays/merge.ts +++ b/packages/support/src/arrays/merge.ts @@ -1,38 +1,171 @@ -import { ArrayMergeError } from "./exceptions"; -import { getErrorMessage } from "@aedart/support/exceptions"; +import type { ArrayMerger as ArrayMergerContract } from "@aedart/contracts/support/arrays"; +import ArrayMerger from "./merge/ArrayMerger"; /** - * Merge two or more arrays + * Returns new Array Merger instance * + * @return {ArrayMerger} + */ +export function merge(): ArrayMergerContract; + +/** + * Returns a merger of given source arrays + * + * @template SourceA extends any[] + * + * @param {SourceA} a + * + * @returns {SourceA} + * + * @throws {ArrayMergeException} + */ +export function merge< + SourceA extends any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ +>(a: SourceA): SourceA; + +/** + * Returns a merger of given source arrays + * + * @template SourceA extends any[] + * @template SourceB extends any[] + * + * @param {SourceA} a + * @param {SourceB} b + * + * @returns {SourceA & SourceB} + * + * @throws {ArrayMergeException} + */ +export function merge< + SourceA extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceB extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ +>(a: SourceA, b: SourceB): SourceA & SourceB; + +/** + * Returns a merger of given source arrays + * + * @template SourceA extends any[] + * @template SourceB extends any[] + * @template SourceC extends any[] + * + * @param {SourceA} a + * @param {SourceB} b + * @param {SourceC} c + * + * @returns {SourceA & SourceB & SourceC} + * + * @throws {ArrayMergeException} + */ +export function merge< + SourceA extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceB extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceC extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ +>(a: SourceA, b: SourceB, c: SourceC): SourceA & SourceB & SourceC; + +/** + * Returns a merger of given source arrays + * + * @template SourceA extends any[] + * @template SourceB extends any[] + * @template SourceC extends any[] + * @template SourceD extends any[] + * + * @param {SourceA} a + * @param {SourceB} b + * @param {SourceC} c + * @param {SourceD} d + * + * @returns {SourceA & SourceB & SourceC & SourceD} + * + * @throws {ArrayMergeException} + */ +export function merge< + SourceA extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceB extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceC extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceD extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ +>(a: SourceA, b: SourceB, c: SourceC, d: SourceD): SourceA & SourceB & SourceC & SourceD; + +/** + * Returns a merger of given source arrays + * + * @template SourceA extends any[] + * @template SourceB extends any[] + * @template SourceC extends any[] + * @template SourceD extends any[] + * @template SourceE extends any[] + * + * @param {SourceA} a + * @param {SourceB} b + * @param {SourceC} c + * @param {SourceD} d + * @param {SourceE} e + * + * @returns {SourceA & SourceB & SourceC & SourceD & SourceE} + * + * @throws {ArrayMergeException} + */ +export function merge< + SourceA extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceB extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceC extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceD extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceE extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ +>(a: SourceA, b: SourceB, c: SourceC, d: SourceD, e: SourceE): SourceA & SourceB & SourceC & SourceD & SourceE; + +/** + * Returns a merger of given source arrays + * + * @template SourceA extends any[] + * @template SourceB extends any[] + * @template SourceC extends any[] + * @template SourceD extends any[] + * @template SourceE extends any[] + * @template SourceF extends any[] + * + * @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 {ArrayMergeException} + */ +export function merge< + SourceA extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceB extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceC extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceD extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceE extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + SourceF extends any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ +>(a: SourceA, b: SourceB, c: SourceC, d: SourceD, e: SourceE, f: SourceF): SourceA & SourceB & SourceC & SourceD & SourceE & SourceF; + +/** + * 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}_ - * + * * @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[]} - * - * @throws {ArrayMergeError} If unable to merge arrays, e.g. if a value cannot be cloned via `structuredClone()` + * + * @return {ArrayMerger|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 = getErrorMessage(e); - - throw new ArrayMergeError('Unable to merge arrays: ' + reason, { - cause: { - previous: e, - sources: sources - } - }); + const merger = new ArrayMerger(); + + if (sources.length == 0) { + return merger as ArrayMergerContract; } + + return merger.of(...sources); } \ No newline at end of file diff --git a/packages/support/src/arrays/merge/ArrayMerger.ts b/packages/support/src/arrays/merge/ArrayMerger.ts new file mode 100644 index 00000000..c6cb6cfa --- /dev/null +++ b/packages/support/src/arrays/merge/ArrayMerger.ts @@ -0,0 +1,109 @@ +import type { + ArrayMerger as ArrayMergerContract, + ArrayMergeOptions, + ArrayMergeCallback +} from "@aedart/contracts/support/arrays"; +import DefaultArrayMergeOptions from './DefaultArrayMergeOptions'; +import {getErrorMessage} from "@aedart/support/exceptions"; +import {ArrayMergeError} from "@aedart/support/arrays"; + +/** + * Array Merger + */ +export default class ArrayMerger implements ArrayMergerContract +{ + /** + * Merge options to be applied + * + * @type {Readonly} + * + * @private + */ + #options: Readonly; + + /** + * Create new Array Merger instance + * + * @param {ArrayMergeCallback | ArrayMergeOptions} [options] + */ + public constructor(options?: ArrayMergeCallback | ArrayMergeOptions) { + // @ts-expect-error Need to init options, however they are resolved via "using". + this.#options = null; + + this.using(options); + } + + /** + * Use the following merge options + * + * @param {ArrayMergeCallback | ArrayMergeOptions} [options] + * + * @return {this} + * + * @throws {ArrayMergeException} + */ + using(options?: ArrayMergeCallback | ArrayMergeOptions): this + { + this.#options = this.resolveOptions(options); + + return this; + } + + /** + * Merge options to be applied + * + * @type {Readonly} + */ + public get options(): Readonly + { + return this.#options; + } + + /** + * Returns a merger of given source arrays + * + * @param {...any[]} sources + * + * @return {any[]} + * + * @throws {ArrayMergeException} + */ + public of(...sources: any[]): any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + try { + const options = this.options; + const callback = (options.callback as ArrayMergeCallback).bind(this); + + // 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 [].concat(...sources).map((element, index, array) => { + return callback(element, index, array, options); + }); + } catch (e) { + const reason = getErrorMessage(e); + + throw new ArrayMergeError('Unable to merge arrays: ' + reason, { + cause: { + previous: e, + sources: sources + } + }); + } + } + + /** + * Resolves options + * + * @param {ArrayMergeCallback | ArrayMergeOptions} options + * + * @return {Readonly} + * + * @protected + */ + protected resolveOptions(options?: ArrayMergeCallback| ArrayMergeOptions): Readonly + { + return DefaultArrayMergeOptions.from(options); + } +} \ No newline at end of file diff --git a/packages/support/src/arrays/merge/DefaultArrayMergeOptions.ts b/packages/support/src/arrays/merge/DefaultArrayMergeOptions.ts new file mode 100644 index 00000000..302ad95d --- /dev/null +++ b/packages/support/src/arrays/merge/DefaultArrayMergeOptions.ts @@ -0,0 +1,63 @@ +import type { + ArrayMergeCallback, + ArrayMergeOptions +} from "@aedart/contracts/support/arrays"; +import { populate } from "@aedart/support/objects"; +import { defaultArrayMergeCallback } from "./defaultArrayMergeCallback"; + +/** + * Default Array Merge Options + */ +export default class DefaultArrayMergeOptions implements ArrayMergeOptions +{ + /** + * Transfer functions + * + * **When `true`**: _functions are transferred into resulting array._ + * + * **When `false` (_default behaviour_)**: _The merge operation will fail when a function + * is encountered (functions are not cloneable by default)._ + * + * @type {boolean} + */ + transferFunctions: boolean = false; + + /** + * Merge callback to be applied + * + * **Note**: _When no callback is provided, then the merge function's default + * callback is used._ + */ + callback: ArrayMergeCallback; + + /** + * Create new default merge options from given options + * + * @param {ArrayMergeCallback | ArrayMergeOptions} [options] + */ + constructor(options?: ArrayMergeCallback | ArrayMergeOptions) { + // Merge provided options, if any given + if (options && typeof options == 'object') { + populate(this, options); + } + + // Resolve merge callback + this.callback = (options && typeof options == 'function') + ? options + : defaultArrayMergeCallback; + } + + /** + * Create new default merge options from given options + * + * @param {ArrayMergeOptions} [options] + * + * @return {Readonly} + */ + public static from(options?: ArrayMergeCallback | ArrayMergeOptions): Readonly + { + const resolved = new this(options); + + return Object.freeze(resolved); + } +} \ No newline at end of file diff --git a/packages/support/src/arrays/merge/defaultArrayMergeCallback.ts b/packages/support/src/arrays/merge/defaultArrayMergeCallback.ts new file mode 100644 index 00000000..c73893b4 --- /dev/null +++ b/packages/support/src/arrays/merge/defaultArrayMergeCallback.ts @@ -0,0 +1,27 @@ +import type { ArrayMergeCallback, ArrayMergeOptions } from "@aedart/contracts/support/arrays"; + +/** + * Default Array Merge callback + * + * @param {any} element + * @param {number} index + * @param {any[]} array + * @param {Readonly} options + * + * @return {any} + */ +export const defaultArrayMergeCallback: ArrayMergeCallback = function( + element: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ + index: number, + array: any[], /* eslint-disable-line @typescript-eslint/no-explicit-any */ + options: Readonly +): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +{ + // Transfer function, if requested by options. + if (options.transferFunctions) { + return element; + } + + // Otherwise, create a structured clone of the element + return structuredClone(element); +} \ No newline at end of file diff --git a/packages/support/src/arrays/merge/index.ts b/packages/support/src/arrays/merge/index.ts new file mode 100644 index 00000000..523096c0 --- /dev/null +++ b/packages/support/src/arrays/merge/index.ts @@ -0,0 +1,8 @@ +import ArrayMerger from './ArrayMerger'; +import DefaultArrayMergeOptions from './DefaultArrayMergeOptions'; +export { + ArrayMerger, + DefaultArrayMergeOptions +} + +export * from './defaultArrayMergeCallback'; \ No newline at end of file From 01cad929dbaf47ccb00e0edc79dccd7e69a8e6d1 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 27 Mar 2024 14:00:03 +0100 Subject: [PATCH 142/224] Change release notes --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b9bea01..db5e11e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,8 +33,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `@typescript-eslint/eslint-plugin` upgraded to `^7.1.1`, in root package. * Removed decorator return types for `use()`, `meta()`, `targetMeta()`, and `inheritTargetMeta()` (_continued to cause TS1270 and TS1238 errors_). [#8](https://github.com/aedart/ion/pull/8), [#9](https://github.com/aedart/ion/pull/9). * Refactored `hasAllMethods()` to use new `isMethod()` internally, in `@aedart/support/reflections`. -* Refactored all components that used deprecated `ConstructorOrAbstractConstructor` to use new `ConstructorLike` type alias. +* Refactored all components that used deprecated `ConstructorOrAbstractConstructor` to use new `ConstructorLike` type alias. * Marked `isClassConstructor()` and `isCallable()` as stable, in `@aedart/support/reflections`. +* Refactored / redesigned the array `merge()` to use a new `ArrayMerger` component, that allows custom merge callback and options. ### Fixed From 3dcf9de1253b311639388e15fbc1bfe99a2ee4c8 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 27 Mar 2024 14:17:28 +0100 Subject: [PATCH 143/224] Rename to Merger Seems a bit more fitting for the actual implementation of the interface. --- packages/support/src/arrays/index.ts | 2 +- packages/support/src/arrays/merge.ts | 10 +++++----- .../src/arrays/merge/{ArrayMerger.ts => Merger.ts} | 4 ++-- packages/support/src/arrays/merge/index.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) rename packages/support/src/arrays/merge/{ArrayMerger.ts => Merger.ts} (96%) diff --git a/packages/support/src/arrays/index.ts b/packages/support/src/arrays/index.ts index 984a2c1f..0736f49a 100644 --- a/packages/support/src/arrays/index.ts +++ b/packages/support/src/arrays/index.ts @@ -6,5 +6,5 @@ export * from './isSafeArrayLike'; export * from './isTypedArray'; export * from './merge'; -export * from './merge'; +export * from './merge/index'; 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 index 06401562..9c2b01e0 100644 --- a/packages/support/src/arrays/merge.ts +++ b/packages/support/src/arrays/merge.ts @@ -1,12 +1,12 @@ -import type { ArrayMerger as ArrayMergerContract } from "@aedart/contracts/support/arrays"; -import ArrayMerger from "./merge/ArrayMerger"; +import type { ArrayMerger } from "@aedart/contracts/support/arrays"; +import Merger from "./merge/Merger"; /** * Returns new Array Merger instance * * @return {ArrayMerger} */ -export function merge(): ArrayMergerContract; +export function merge(): ArrayMerger; /** * Returns a merger of given source arrays @@ -161,10 +161,10 @@ export function merge( ...sources: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ ) { - const merger = new ArrayMerger(); + const merger = new Merger(); if (sources.length == 0) { - return merger as ArrayMergerContract; + return merger as ArrayMerger; } return merger.of(...sources); diff --git a/packages/support/src/arrays/merge/ArrayMerger.ts b/packages/support/src/arrays/merge/Merger.ts similarity index 96% rename from packages/support/src/arrays/merge/ArrayMerger.ts rename to packages/support/src/arrays/merge/Merger.ts index c6cb6cfa..e9733104 100644 --- a/packages/support/src/arrays/merge/ArrayMerger.ts +++ b/packages/support/src/arrays/merge/Merger.ts @@ -1,5 +1,5 @@ import type { - ArrayMerger as ArrayMergerContract, + ArrayMerger, ArrayMergeOptions, ArrayMergeCallback } from "@aedart/contracts/support/arrays"; @@ -10,7 +10,7 @@ import {ArrayMergeError} from "@aedart/support/arrays"; /** * Array Merger */ -export default class ArrayMerger implements ArrayMergerContract +export default class Merger implements ArrayMerger { /** * Merge options to be applied diff --git a/packages/support/src/arrays/merge/index.ts b/packages/support/src/arrays/merge/index.ts index 523096c0..d1ada0bd 100644 --- a/packages/support/src/arrays/merge/index.ts +++ b/packages/support/src/arrays/merge/index.ts @@ -1,7 +1,7 @@ -import ArrayMerger from './ArrayMerger'; +import Merger from './Merger'; import DefaultArrayMergeOptions from './DefaultArrayMergeOptions'; export { - ArrayMerger, + Merger, DefaultArrayMergeOptions } From 9f8e3d961d44fd7e6398ea5e24904a4ba0420a53 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 27 Mar 2024 14:17:47 +0100 Subject: [PATCH 144/224] Add tests for custom merge options and callback --- .../packages/support/arrays/merge.test.js | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/tests/browser/packages/support/arrays/merge.test.js b/tests/browser/packages/support/arrays/merge.test.js index c25f5712..f20d304c 100644 --- a/tests/browser/packages/support/arrays/merge.test.js +++ b/tests/browser/packages/support/arrays/merge.test.js @@ -1,4 +1,8 @@ -import { ArrayMergeError, merge } from "@aedart/support/arrays"; +import { + ArrayMergeError, + merge, + Merger +} from "@aedart/support/arrays"; describe('@aedart/support/arrays', () => { describe('merge()', () => { @@ -60,5 +64,56 @@ describe('@aedart/support/arrays', () => { expect(callback) .toThrowError(ArrayMergeError); }); + + it('returns merger object when no args given', () => { + const merger = merge(); + + expect(merger) + .toBeInstanceOf(Merger); + }); + + it('can transfer functions', () => { + + const fnA = () => false; + const fnB = () => true; + + const a = [ fnA ]; + const b = [ fnB ]; + + // --------------------------------------------------------------- // + + const result = merge() + .using({ transferFunctions: true }) + .of(a, b); + + expect(result.length) + .withContext('Incorrect amount of elements in output') + .toBe(2); + + expect(result[0]) + .withContext('Function A not transferred') + .toBe(fnA); + + expect(result[1]) + .withContext('Function B not transferred') + .toBe(fnB); + }); + + it('can apply custom merge callback', () => { + + const a = [ 1, 2, 3 ]; + const b = [ 4, 5, 6 ]; + + // --------------------------------------------------------------- // + + const result = merge() + .using((element) => { + return element * 2; + }) + .of(a, b); + + expect(result) + .toEqual([ 2, 4, 6, 8, 10, 12 ]); + }); }); }); \ No newline at end of file From 850bf7b43ee606d57806d322798c3a72f1712638 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 09:15:55 +0200 Subject: [PATCH 145/224] Add merge options docs, for array merge --- .../current/packages/support/arrays/merge.md | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/docs/archive/current/packages/support/arrays/merge.md b/docs/archive/current/packages/support/arrays/merge.md index c7a54b64..2c210845 100644 --- a/docs/archive/current/packages/support/arrays/merge.md +++ b/docs/archive/current/packages/support/arrays/merge.md @@ -6,6 +6,8 @@ sidebarDepth: 0 # `merge` +[[TOC]] + 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). @@ -21,7 +23,7 @@ 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). +Simple (_or "plain"_) objects are [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 @@ -48,4 +50,61 @@ const a = [ 1, 2, 3 ]; const b = [ function() {} ]; // A function cannot be deep copied... merge(a, b); // ArrayMergeError -``` \ No newline at end of file +``` + +_See [merge options](#merge-options) for details on how to deal with functions._ + +## Merge Options + +`merge()` supports a number of options. To specify thom, use the `using()` method. + +```js +merge() + .using({ /** option: value */ }) + .of(arrayA, arrayB, arrayC); +``` + +::: tip Note +When invoking `merge()` without any arguments, an underlying array `Merger` instance is returned. +::: + +### `transferFunctions` + +By default, functions are not transferred (_not copied_). When encountered an `ArrayMergeError` is thrown, because +the underlying [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) is not able to +duplicate functions. To change this behaviour, you can set the `transferFunctions` setting to `true`. Function are then +"transferred" into the resulting array. + +```js +const foo = () => true; +const bar = () => false; + +merge() + .using({ transferFunctions: true }) + .of([ foo ], [ bar ]) // [ foo, bar ] +``` + +### `callback` + +If you require more advanced duplication logic of the array values, then you can specify a callback that can process and +return the value in question. + +```js +const a = [ 1, 2 ]; +const b = [ 3, 4 ]; + +const result = merge() + .using({ + callback: (element, index, array, options) => { + return element * 2; + } + }) + .of(a, b); // [ 2, 4, 6, 8 ] +``` + +#### Arguments + +* `element: any` The current element being processed in the array. +* `index: number` The index of the current element being processed in the array. +* `array: any[]` The concatenated array this callback was called upon. +* `options: Readonly` The merge options to be applied. \ No newline at end of file From a556cae7ead62dddaee82415d80d67ec949a0a6c Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 09:44:33 +0200 Subject: [PATCH 146/224] Change formatting of arguments --- docs/archive/current/packages/support/arrays/merge.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/archive/current/packages/support/arrays/merge.md b/docs/archive/current/packages/support/arrays/merge.md index 2c210845..17055106 100644 --- a/docs/archive/current/packages/support/arrays/merge.md +++ b/docs/archive/current/packages/support/arrays/merge.md @@ -104,7 +104,7 @@ const result = merge() #### Arguments -* `element: any` The current element being processed in the array. -* `index: number` The index of the current element being processed in the array. -* `array: any[]` The concatenated array this callback was called upon. -* `options: Readonly` The merge options to be applied. \ No newline at end of file +* `element: any` - The current element being processed in the array. +* `index: number` - The index of the current element being processed in the array. +* `array: any[]` - The concatenated array this callback was called upon. +* `options: Readonly` - The merge options to be applied. \ No newline at end of file From cb5403022d095112348255c7160a0ece4c639309 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 09:58:35 +0200 Subject: [PATCH 147/224] Add array merge options, in object merge --- .../contracts/src/support/objects/merge/MergeOptions.ts | 8 ++++++++ packages/support/src/objects/merge/DefaultMergeOptions.ts | 8 ++++++++ .../support/src/objects/merge/defaultMergeCallback.ts | 8 ++++++-- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/support/objects/merge/MergeOptions.ts b/packages/contracts/src/support/objects/merge/MergeOptions.ts index 2d1dd6b4..f081c893 100644 --- a/packages/contracts/src/support/objects/merge/MergeOptions.ts +++ b/packages/contracts/src/support/objects/merge/MergeOptions.ts @@ -2,6 +2,7 @@ import type { MergeCallback, SkipKeyCallback } from "./types"; +import type { ArrayMergeOptions } from "@aedart/contracts/support/arrays"; /** * Merge Options @@ -128,6 +129,13 @@ export default interface MergeOptions */ mergeArrays?: boolean; + /** + * Merge Options for arrays + * + * @type {ArrayMergeOptions} + */ + arrayMergeOptions?: ArrayMergeOptions; + /** * The merge callback that must be applied * diff --git a/packages/support/src/objects/merge/DefaultMergeOptions.ts b/packages/support/src/objects/merge/DefaultMergeOptions.ts index a96776a3..48787536 100644 --- a/packages/support/src/objects/merge/DefaultMergeOptions.ts +++ b/packages/support/src/objects/merge/DefaultMergeOptions.ts @@ -3,6 +3,7 @@ import type { MergeOptions, SkipKeyCallback } from "@aedart/contracts/support/objects"; +import type { ArrayMergeOptions } from "@aedart/contracts/support/arrays"; import { DEFAULT_MAX_MERGE_DEPTH } from "@aedart/contracts/support/objects"; import { MergeError } from "../exceptions"; import { defaultMergeCallback } from "./defaultMergeCallback"; @@ -136,6 +137,13 @@ export default class DefaultMergeOptions implements MergeOptions */ mergeArrays: boolean = false; + /** + * Merge Options for arrays + * + * @type {ArrayMergeOptions} + */ + arrayMergeOptions: ArrayMergeOptions = {}; + /** * The merge callback that must be applied * diff --git a/packages/support/src/objects/merge/defaultMergeCallback.ts b/packages/support/src/objects/merge/defaultMergeCallback.ts index a2fb4e48..8a019e3a 100644 --- a/packages/support/src/objects/merge/defaultMergeCallback.ts +++ b/packages/support/src/objects/merge/defaultMergeCallback.ts @@ -81,11 +81,15 @@ export const defaultMergeCallback: MergeCallback = function(target: MergeSourceI ) { // If either existing or new value is of the type array, merge values into // a new array. - return mergeArrays(existingValue, value); + return mergeArrays() + .using(options.arrayMergeOptions) + .of(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); + return mergeArrays() + .using(options.arrayMergeOptions) + .of(value); } // For concat spreadable objects or array-like objects, the "basic object" merge logic From bf47a89630dd679803128dfce8d299923d74153e Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 09:58:43 +0200 Subject: [PATCH 148/224] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db5e11e6..c909bbe9 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 * `CallbackWrapper` util class, in `@aedart/support`. * `isCallbackWrapper` util, in `@aedart/support`. * `ArbitraryData` concern, in `@aedart/support`. +* `arrayMergeOptions` in object `merge()`. * Add upgrade guide from v0.7.x- to v0.10.x. ### Changed From 90a7f650db591966310e70361b755722f7f01a11 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 10:07:13 +0200 Subject: [PATCH 149/224] Add ref. to array merge options, for object merge --- docs/archive/current/packages/support/objects/merge.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/archive/current/packages/support/objects/merge.md b/docs/archive/current/packages/support/objects/merge.md index bc3e00b4..aaddcce1 100644 --- a/docs/archive/current/packages/support/objects/merge.md +++ b/docs/archive/current/packages/support/objects/merge.md @@ -297,6 +297,10 @@ merge() Behind the scene, the [array merge](../arrays/merge.md) utility is used for merging arrays. +### `arrayMergeOptions` + +_See [Array Merge Options](../arrays/merge.md#merge-options)._ + ### `callback` In situations when you need more advanced merge logic, you may specify a custom callback. From 04bff8c326c19f22a05126fe8928eee8ff575334 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 10:47:01 +0200 Subject: [PATCH 150/224] Fix unable to merge arrays containing functions --- packages/support/src/meta/MetaRepository.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/support/src/meta/MetaRepository.ts b/packages/support/src/meta/MetaRepository.ts index 54684b76..81104909 100644 --- a/packages/support/src/meta/MetaRepository.ts +++ b/packages/support/src/meta/MetaRepository.ts @@ -245,10 +245,13 @@ export default class MetaRepository implements Repository get: () => { // To ensure that metadata cannot be changed outside the scope and context of a // meta decorator, a deep clone of the record is returned here. - return merge( - Object.create(null), - registry.get(owner) || Object.create(null) - ); + return merge() + .using({ + arrayMergeOptions: { + transferFunctions: true + } + }) + .of(Object.create(null), registry.get(owner) || Object.create(null)) }, // Ensure that the property cannot be deleted From 0aff65232a8f140e48b85e8d6a34eab00d25a2cf Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 10:49:17 +0200 Subject: [PATCH 151/224] Change build test, use classes as dependencies directly This shows that the meta repository is able to merge arrays with "functions" (constructors) and can therefore be stated as dependencies directly. This will become important at a later point, e.g. when specific "concrete" instances are desired resolved from the service container. --- .../packages/container/container/build.test.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/browser/packages/container/container/build.test.js b/tests/browser/packages/container/container/build.test.js index a34dca79..cef0d04c 100644 --- a/tests/browser/packages/container/container/build.test.js +++ b/tests/browser/packages/container/container/build.test.js @@ -1,7 +1,7 @@ import {Container, BindingEntry, ContainerError, CircularDependencyError } from "@aedart/container"; import { dependencies } from "@aedart/support/container"; -describe('@aedart/support/container', () => { +fdescribe('@aedart/support/container', () => { describe('build', () => { it('fails when concrete is not buildable', () => { @@ -121,8 +121,8 @@ describe('@aedart/support/container', () => { const container = new Container(); class A {} - - @dependencies('a') + + @dependencies(A) class B { item; @@ -130,8 +130,8 @@ describe('@aedart/support/container', () => { this.item = item; } } - - @dependencies('b') + + @dependencies(B) class C { item; @@ -139,10 +139,6 @@ describe('@aedart/support/container', () => { this.item = item; } } - - container - .bind('a', A) - .bind('b', B); // -------------------------------------------------------------------- // From 44320401f61574e49cf0341a7d75fa7c394def74 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 10:49:25 +0200 Subject: [PATCH 152/224] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c909bbe9..94c80743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Decorator types aliases (_TS1270 and TS1238 issues when applying the various decorator and decorator result types_). [#8](https://github.com/aedart/ion/pull/8). * Broken link in docs for `isArrayLike`. * Missing `tslib` as peer dependency for `@aedart/support` package. +* Unable to merge arrays containing functions, in `MetaRepository`. ### Deprecated From bf01df7de0fe9e27dbdcf5b510a4286bd1fa58bd Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 11:14:47 +0200 Subject: [PATCH 153/224] Cleanup --- tests/browser/packages/container/container/build.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/packages/container/container/build.test.js b/tests/browser/packages/container/container/build.test.js index cef0d04c..5fb55b77 100644 --- a/tests/browser/packages/container/container/build.test.js +++ b/tests/browser/packages/container/container/build.test.js @@ -1,7 +1,7 @@ import {Container, BindingEntry, ContainerError, CircularDependencyError } from "@aedart/container"; import { dependencies } from "@aedart/support/container"; -fdescribe('@aedart/support/container', () => { +describe('@aedart/support/container', () => { describe('build', () => { it('fails when concrete is not buildable', () => { From f8c8ccdfa83ef2ccfe50828ce0221e31e1ff20a8 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 11:35:41 +0200 Subject: [PATCH 154/224] Fix dependencies not resolved for callback-wrapper --- packages/container/src/Container.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index daf4b655..d06c41dd 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -910,7 +910,7 @@ export default class Container implements ServiceContainerContract && wrapper['has'](DEPENDENCIES) ) { /* @ts-expect-error TS7053 - get method is in wrapper at this point */ - const dependencies: Identifier[] = wrapper['get']() ?? []; + const dependencies: Identifier[] = wrapper['get'](DEPENDENCIES) ?? []; const resolved: any[] = []; /* eslint-disable-line @typescript-eslint/no-explicit-any */ for(const identifier of dependencies) { resolved.push(this.resolveDependency(identifier, wrapper)); From 128f0f66e50d9b8142c78ee31b866aa5749a2aeb Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 12:49:59 +0200 Subject: [PATCH 155/224] Fix dependencies are not resolved for class method when called --- packages/container/src/Container.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index d06c41dd..e625f35a 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -26,7 +26,10 @@ import { getNameOrDesc, hasAllMethods, } from "@aedart/support/reflections"; -import { isCallbackWrapper } from "@aedart/support"; +import { + isCallbackWrapper, + CallbackWrapper as Wrapper +} from "@aedart/support"; import CircularDependencyError from "./exceptions/CircularDependencyError"; import ContainerError from "./exceptions/ContainerError"; import NotFoundError from "./exceptions/NotFoundError"; @@ -875,8 +878,17 @@ export default class Container implements ServiceContainerContract const name: ClassMethodName = reference[1]; const method: Callback = target[name]; + + // Resolve evt. dependencies if no arguments are given... + if (args.length == 0 && this.hasDependencies(method as object)) { + args = this.resolveDependencies(method as object); + } - return this.invokeCallback(method.bind(target), args); + // Wrap the class method into a callback-wrapper, such that the "ThisArg" can be + // applied correctly. + return this.invokeCallbackWrapper( + Wrapper.makeFor(target, method, args) + ); } /** @@ -938,8 +950,7 @@ export default class Container implements ServiceContainerContract protected invokeCallback(callback: Callback, args: any[] = []): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { // When no arguments are provided, attempt to obtain and resolve dependencies - // for the callback. This will work if the callback is a class method with - // defined dependencies. Otherwise, this will not do anything... + // for the callback (if any dependencies are defined for the callback). if (args.length == 0 && this.hasDependencies(callback as object)) { args = this.resolveDependencies(callback as object); } From 6216e203822d4f3aaecc741c85be470ea2242e4c Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 12:50:14 +0200 Subject: [PATCH 156/224] Add tests for call() --- .../packages/container/container/call.test.js | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 tests/browser/packages/container/container/call.test.js diff --git a/tests/browser/packages/container/container/call.test.js b/tests/browser/packages/container/container/call.test.js new file mode 100644 index 00000000..593f5aa0 --- /dev/null +++ b/tests/browser/packages/container/container/call.test.js @@ -0,0 +1,137 @@ +import {Container, ContainerError } from "@aedart/container"; +import { DEPENDENCIES } from "@aedart/contracts/container"; +import { dependencies } from "@aedart/support/container"; +import { CallbackWrapper } from "@aedart/support"; + +describe('@aedart/support/container', () => { + describe('call', () => { + + it('fails when unsupported type given as "method" argument', () => { + const container = new Container(); + + const callback = () => { + container.call('uknown'); + }; + + expect(callback) + .toThrowError(ContainerError); + }); + + it('can call callback', () => { + const container = new Container(); + + const callback = (a) => a; + + // --------------------------------------------------------------------------- // + + const arg = 'lipsum'; + const result = container.call(callback, [ arg ]); + + expect(result) + .toBe(arg); + }); + + it('can call callback-wrapper', () => { + const container = new Container(); + + const wrapper = CallbackWrapper.make((a) => a); + + // --------------------------------------------------------------------------- // + + const arg = 'esto buno'; + const result = container.call(wrapper, [ arg ]); + + expect(result) + .toBe(arg); + }); + + it('resolves callback-wrapper dependencies', () => { + const container = new Container(); + + class A {} + + const wrapper = CallbackWrapper + .make((a) => a) + .set(DEPENDENCIES, [ A ]); + + // --------------------------------------------------------------------------- // + + const result = container.call(wrapper); + + expect(result) + .toBeInstanceOf(A); + }); + + it('applies callback-wrapper\'s own arguments', () => { + const container = new Container(); + + class A {} + + const wrapper = CallbackWrapper + .make((a) => a, [ new A() ]); + + // --------------------------------------------------------------------------- // + + const result = container.call(wrapper); + + expect(result) + .toBeInstanceOf(A); + }); + + it('can call class method', () => { + const container = new Container(); + + class A { + hi(name) { + return name; + } + } + + // --------------------------------------------------------------------------- // + + const name = 'Ofelia'; + const result = container.call([ A, 'hi' ], [ name ]); + + expect(result) + .toBe(name); + }); + + it('resolves class method dependencies', () => { + const container = new Container(); + + class Api {} + class Message {} + + + @dependencies(Api) + class Service { + api; + + constructor(api) { + this.api = api; + } + + @dependencies(Message) + send(message) { + return [ this.api, message ]; + } + } + + // --------------------------------------------------------------------------- // + + const result = container.call([ Service, 'send' ]); + + expect(result.length) + .withContext('Incorrect output') + .toBe(2); + + expect(result[0]) + .withContext('Dependency (api) not resolved') + .toBeInstanceOf(Api); + + expect(result[1]) + .withContext('Dependency (message) not resolved') + .toBeInstanceOf(Message); + }); + }); +}); \ No newline at end of file From 37346297e8b300a7656064d9cdd147288bdc9881 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 13:14:34 +0200 Subject: [PATCH 157/224] Add tests for extend() --- .../container/container/extend.test.js | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 tests/browser/packages/container/container/extend.test.js diff --git a/tests/browser/packages/container/container/extend.test.js b/tests/browser/packages/container/container/extend.test.js new file mode 100644 index 00000000..f4b5111c --- /dev/null +++ b/tests/browser/packages/container/container/extend.test.js @@ -0,0 +1,111 @@ +import { Container } from "@aedart/container"; + +describe('@aedart/support/container', () => { + describe('extend', () => { + + it('can extend existing binding', () => { + const container = new Container(); + + class A { + foo; + } + + container + .bind('a', () => new A()) + .extend('a', (resolved) => { + resolved.foo = 'bar'; + + return resolved; + }) + + // ----------------------------------------------------------------- // + + const result = container.make('a'); + + expect(result) + .toBeInstanceOf(A); + + expect(result.foo) + .toBe('bar'); + }); + + it('can extend shared instance', () => { + const container = new Container(); + + class A { + name; + } + + container + .singleton('a', () => new A()); + + // ----------------------------------------------------------------- // + + const first = container.make('a'); + + // ----------------------------------------------------------------- // + + container.extend('a', (resolved) => { + resolved.name = 'Jimmy'; + + return resolved; + }); + + // ----------------------------------------------------------------- // + + const second = container.make('a'); + + expect(first) + .withContext('First not of correct instance') + .toBeInstanceOf(A); + expect(second) + .withContext('Second not of correct instance') + .toBeInstanceOf(A); + + expect(first) + .withContext('First and second instance SHOULD be the same') + .toBe(second); + + expect(second.name) + .toBe('Jimmy'); + }); + + it('can replace shared instance via extend', () => { + const container = new Container(); + + class A {} + class B {} + + container + .singleton('a', () => new A()); + + // ----------------------------------------------------------------- // + + const first = container.make('a'); + + // ----------------------------------------------------------------- // + + container.extend('a', () => { + // This might be unusual to do, but possible! + return new B(); + }); + + // ----------------------------------------------------------------- // + + const second = container.make('a'); + + expect(first) + .withContext('First incorrect') + .toBeInstanceOf(A); + + expect(second) + .withContext('Second incorrect') + .toBeInstanceOf(B); + + expect(first) + .withContext('First and second instance SHOULD NOT be the same') + .not + .toBe(second); + }); + }); +}); \ No newline at end of file From a216bced2580c4e774f2dd07fc85d1d8e2029335 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 13:17:45 +0200 Subject: [PATCH 158/224] Change order and semantics of rebound callback arguments --- packages/container/src/Container.ts | 4 ++-- packages/contracts/src/container/types.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index e625f35a..6619ddeb 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -1033,11 +1033,11 @@ export default class Container implements ServiceContainerContract */ protected rebound(identifier: Identifier): void { - const instance = this.make(identifier); + const resolved = this.make(identifier); const callbacks: ReboundCallback[] = this.getReboundCallbacks(identifier); for (const callback of callbacks) { - callback(this, instance); + callback(resolved, this); } } diff --git a/packages/contracts/src/container/types.ts b/packages/contracts/src/container/types.ts index 87d0d004..de546f69 100644 --- a/packages/contracts/src/container/types.ts +++ b/packages/contracts/src/container/types.ts @@ -43,8 +43,8 @@ export type ExtendCallback< * Callback to be invoked when a binding is "rebound". */ export type ReboundCallback< - Instance = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ -> = (container: Container, instance: Instance) => void; + Value = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +> = (resolved: Value, container: Container) => void; /** * Before Resolved Callback From 9c6c3766f0eba488de5ad8bd93a411f0888f030e Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 13:48:58 +0200 Subject: [PATCH 159/224] Add tests for rebinding() --- .../container/container/rebinding.test.js | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/browser/packages/container/container/rebinding.test.js diff --git a/tests/browser/packages/container/container/rebinding.test.js b/tests/browser/packages/container/container/rebinding.test.js new file mode 100644 index 00000000..17d072ce --- /dev/null +++ b/tests/browser/packages/container/container/rebinding.test.js @@ -0,0 +1,53 @@ +import { Container } from "@aedart/container"; + +describe('@aedart/support/container', () => { + describe('rebinding', () => { + + it('invokes rebound-callback when resolved binding is rebound', () => { + const container = new Container(); + + let wasRebound = false; + + container.rebinding('a', () => { + wasRebound = true; + }); + container.bind('a', () => 'b'); + + // ----------------------------------------------------------------- // + + const result = container.make('a'); + + container.extend('a', (resolved) => resolved); + + expect(result) + .toBe('b'); + expect(wasRebound) + .withContext('Rebound callback not invoked') + .toBeTrue(); + }); + + it('invokes rebound-callback when instance is rebound', () => { + const container = new Container(); + + let wasRebound = false; + + container.rebinding('a', () => { + wasRebound = true; + }); + + class A {} + + // ----------------------------------------------------------------- // + + const result = container.instance('a', new A()); + + container.extend('a', (resolved) => resolved); + + expect(result) + .toBeInstanceOf(A); + expect(wasRebound) + .withContext('Rebound callback not invoked') + .toBeTrue(); + }); + }); +}); \ No newline at end of file From 92de8b014519d95fff723758d504ecfa5ec5e2bf Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 14:13:29 +0200 Subject: [PATCH 160/224] Refactor forget() to use forgetInstance() --- packages/container/src/Container.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 6619ddeb..fa754b10 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -575,10 +575,8 @@ export default class Container implements ServiceContainerContract public forget(identifier: Identifier): boolean { const removedAlias: boolean = this.aliases.delete(identifier); - - identifier = this.getAlias(identifier); - const removedBinding: boolean = this.bindings.delete(identifier); - const removedInstance: boolean = this.instances.delete(identifier); + const removedInstance: boolean = this.forgetInstance(identifier); + const removedBinding: boolean = this.bindings.delete(this.getAlias(identifier)); return removedBinding || removedInstance || removedAlias; } From 1cde06802057d5cefcb68846b37f2c50b7d720f9 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 14:13:41 +0200 Subject: [PATCH 161/224] Add tests for forget() --- .../container/container/forget.test.js | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/browser/packages/container/container/forget.test.js diff --git a/tests/browser/packages/container/container/forget.test.js b/tests/browser/packages/container/container/forget.test.js new file mode 100644 index 00000000..b319539d --- /dev/null +++ b/tests/browser/packages/container/container/forget.test.js @@ -0,0 +1,67 @@ +import { Container } from "@aedart/container"; + +describe('@aedart/support/container', () => { + describe('forget', () => { + + it('can forget binding', () => { + const container = new Container(); + + container.bind('a', () => 'b'); + + // ----------------------------------------------------------------- // + + const result = container.forget('a'); + + expect(result) + .withContext('Identifier is not forgotten') + .toBeTrue(); + + expect(container.has('a')) + .withContext('Binding still exists in container') + .toBeFalse(); + }); + + it('can forget binding alias', () => { + const container = new Container(); + + container + .bind('a', () => 'b') + .alias('a', 'b'); + + // ----------------------------------------------------------------- // + + const result = container.forget('b'); + + expect(result) + .withContext('Identifier is not forgotten') + .toBeTrue(); + + expect(container.has('b')) + .withContext('Binding still exists in container') + .toBeFalse(); + + expect(container.has('a')) + .withContext('Binding (a) SHOULD STILL exists in container') + .toBeTrue(); + }); + + it('can forget instance', () => { + const container = new Container(); + + class A {} + container.instance('a', new A()); + + // ----------------------------------------------------------------- // + + const result = container.forget('a'); + + expect(result) + .withContext('Identifier is not forgotten') + .toBeTrue(); + + expect(container.has('a')) + .withContext('Binding still exists in container') + .toBeFalse(); + }); + }); +}); \ No newline at end of file From eaa28a32454824ddaa63618e6fb5cae396c09c2a Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 14:19:16 +0200 Subject: [PATCH 162/224] Add test for flush() --- .../container/container/flush.test.js | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/browser/packages/container/container/flush.test.js diff --git a/tests/browser/packages/container/container/flush.test.js b/tests/browser/packages/container/container/flush.test.js new file mode 100644 index 00000000..ec427573 --- /dev/null +++ b/tests/browser/packages/container/container/flush.test.js @@ -0,0 +1,37 @@ +import { Container } from "@aedart/container"; + +describe('@aedart/support/container', () => { + describe('flush', () => { + + it('flushes bindings, instances, aliases and resolved', () => { + const container = new Container(); + + container + .bind('a', () => 'b') + .alias('a', 'b'); + + class Foo {} + container.instance('foo', new Foo()); + + // ----------------------------------------------------------------- // + + container.flush(); + + expect(container.has('a')) + .withContext('binding still registered') + .toBeFalse(); + + expect(container.has('b')) + .withContext('alias still registered') + .toBeFalse(); + + expect(container.has('foo')) + .withContext('instance still registered') + .toBeFalse(); + + expect(container.isResolved('foo')) + .withContext('instance still marked as resolved') + .toBeFalse(); + }); + }); +}); \ No newline at end of file From dacc5c63369516872e5fef9d166f97d1d9dbef6b Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 2 Apr 2024 14:26:31 +0200 Subject: [PATCH 163/224] Add test for before() and after() --- .../container/resolved-callbacks.test.js | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/browser/packages/container/container/resolved-callbacks.test.js diff --git a/tests/browser/packages/container/container/resolved-callbacks.test.js b/tests/browser/packages/container/container/resolved-callbacks.test.js new file mode 100644 index 00000000..722c6e5f --- /dev/null +++ b/tests/browser/packages/container/container/resolved-callbacks.test.js @@ -0,0 +1,39 @@ +import { Container } from "@aedart/container"; + +describe('@aedart/support/container', () => { + describe('before / after', () => { + + it('invokes resolved callbacks', () => { + const container = new Container(); + + let beforeInvoked = false; + let afterInvoked = false; + + class Service {} + container + .bind('api', Service) + .before('api', () => { + beforeInvoked = true; + }) + .after('api', () => { + afterInvoked = true; + }); + + + // ----------------------------------------------------------------- // + + const result = container.make('api'); + + expect(result) + .toBeInstanceOf(Service); + + expect(beforeInvoked) + .withContext('before callback not invoked') + .toBeTrue(); + + expect(afterInvoked) + .withContext('after callback not invoked') + .toBeTrue(); + }); + }); +}); \ No newline at end of file From 6c21cf687c30dce9764b2e67742f9be7f76675f9 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 3 Apr 2024 16:13:39 +0200 Subject: [PATCH 164/224] Add Facade abstraction --- packages/support/src/facades/Facade.ts | 321 +++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 packages/support/src/facades/Facade.ts diff --git a/packages/support/src/facades/Facade.ts b/packages/support/src/facades/Facade.ts new file mode 100644 index 00000000..9ef2fb54 --- /dev/null +++ b/packages/support/src/facades/Facade.ts @@ -0,0 +1,321 @@ +import type { Container, Identifier } from "@aedart/contracts/container"; +import type { Callback } from "@aedart/contracts"; +import { isset } from "@aedart/support/misc"; +import { AbstractClassError, LogicalError } from "@aedart/support/exceptions"; + +/** + * Facade + * + * Adaptation of Laravel's Facade abstraction. + * + * @see https://github.com/laravel/framework/blob/master/src/Illuminate/Support/Facades/Facade.php + * + * @abstract + */ +export default abstract class Facade +{ + /** + * The facade's service container instance + * + * @type {Container|undefined} + * + * @private + * @static + */ + static #container: Container | undefined = undefined; + + /** + * Resolved instances + * + * @type {Map} + * + * @private + * @static + */ + static #resolved: Map< + Identifier, + any + > = new Map(); + + /** + * Registered object spies + * + * @type {Set} + * + * @private + * @static + */ + static #spies: Set = new Set(); + + /** + * Facade Constructor + * + * @protected + */ + protected constructor() { + /* @ts-expect-error TS2345 Facade constructor is abstract */ + throw new AbstractClassError(this); + } + + /** + * Returns identifier to be used for resolving facade's instance + * + * @return {Identifier} + * + * @abstract + * + * @static + */ + public static getIdentifier(): Identifier + { + throw new LogicalError('Facade does not implement the getAccessor() method'); + } + + /** + * Obtain the facade's underlying resolved object instance, or object "spy" + * + * @return {any} + * + * @throws {NotFoundException} + * @throws {ContainerException} + * @throws {LogicalError} + * + * @abstract + * + * @static + */ + public static obtain() + { + throw new LogicalError('Facade does not implement the get() method'); + } + + /** + * Register a "spy" (e.g. object mock) for this facade's identifier + * + * @param {Callback} callback Callback to be used for creating some kind of object spy + * or mock, with appropriate configuration and expectations for testing + * purposes. + * + * @return {ReturnType} + * + * @static + */ + public static spy(callback: Callback): ReturnType + { + const identifier = this.getIdentifier(); + const spy = callback(identifier); + + this.swap(spy); + + this.#spies.add(identifier); + + return spy; + } + + /** + * Determine if a spy has been registered for this facade's identifier + * + * @return {boolean} + * + * @static + */ + public static isSpy(): boolean + { + return this.#spies.has(this.getIdentifier()); + } + + /** + * Removes registered spy for this facade's identifier + * + * **Warning**: _Method does NOT perform any actual "spy" cleanup logic. + * It only removes the reference to the "spy" object._ + * + * @return {boolean} + * + * @static + */ + public static forgetSpy(): boolean + { + const identifier = this.getIdentifier(); + + return this.forgetResolved(identifier) + && this.#spies.delete(identifier); + } + + /** + * Removes all registered spies + * + * @return {void} + * + * @static + */ + public static forgetAllSpies(): void + { + for (const identifier of this.#spies) { + this.forgetResolved(identifier); + } + + this.#spies.clear(); + } + + /** + * Swap the facade's underlying instance + * + * @param {any} instance + * + * @return {void} + * + * @static + */ + public static swap(instance: any): void + { + const identifier = this.getIdentifier(); + + this.#resolved.set(identifier, instance); + + if (this.hasContainer()) { + (this.getContainer() as Container).instance(identifier, instance); + } + } + + /** + * Set this facade's service container instance + * + * @param {Container | undefined} container + * + * @return {this} + * + * @static + */ + public static setContainer(container: Container | undefined): Facade + { + this.#container = container; + + return this; + } + + /** + * Get this facade's service container instance + * + * @return {Container | undefined} + * + * @static + */ + public static getContainer(): Container | undefined + { + return this.#container; + } + + /** + * Determine if facade's has a service container instance set + * + * @return {boolean} + * + * @static + */ + public static hasContainer(): boolean + { + return isset(this.#container); + } + + /** + * Removes this facade's service container instance + * + * @return {this} + * + * @static + */ + public static forgetContainer(): Facade + { + this.#container = undefined; + + return this; + } + + /** + * Determine if resolved facade instance exists + * + * @param {Identifier} identifier + * + * @reurn {boolean} + * + * @static + */ + public static hasResolved(identifier: Identifier): boolean + { + return this.#resolved.has(identifier); + } + + /** + * Forget resolved facade instance + * + * @param {Identifier} identifier + * + * @return {boolean} + * + * @static + */ + public static forgetResolved(identifier: Identifier): boolean + { + return this.#resolved.delete(identifier); + } + + /** + * Forget all resolved facade instances + * + * @return {void} + * + * @static + */ + public static forgetAllResolved(): void + { + this.#resolved.clear(); + } + + /** + * Destroy all resolved facade instances and forget the service container + * + * @return {void} + */ + public static destroyFacades(): void + { + this.forgetAllSpies(); + this.forgetAllResolved(); + this.forgetContainer(); + } + + /** + * Get the facade's underlying object instance or object spy + * + * @template T = any + * + * @param {Identifier} identifier + * + * @return {T} + * + * @throws {NotFoundException} + * @throws {ContainerException} + * @throws {LogicalError} + * + * @protected + * + * @static + */ + protected static resolve< + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(identifier: Identifier): T + { + if (!this.hasContainer()) { + throw new LogicalError('Facade has no service container instance set'); + } + + if (this.hasResolved(identifier)) { + return this.#resolved.get(identifier) as T; + } + + const resolved = (this.getContainer() as Container).make(identifier); + this.#resolved.set(identifier, resolved); + + return resolved; + } +} \ No newline at end of file From ccc68f5c30bec1ea20b9bc9a99391f2cc454961c Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 3 Apr 2024 16:13:48 +0200 Subject: [PATCH 165/224] Add service container facade --- packages/support/src/facades/Container.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packages/support/src/facades/Container.ts diff --git a/packages/support/src/facades/Container.ts b/packages/support/src/facades/Container.ts new file mode 100644 index 00000000..ebd11232 --- /dev/null +++ b/packages/support/src/facades/Container.ts @@ -0,0 +1,21 @@ +import type { Identifier, Container as ServiceContainer } from "@aedart/contracts/container"; +import { CONTAINER } from "@aedart/contracts/container"; +import Facade from "./Facade"; + +/** + * Container Facade + * + * @extends Facade + */ +export default class Container extends Facade +{ + public static getIdentifier(): Identifier + { + return CONTAINER; + } + + public static obtain() + { + return this.resolve(this.getIdentifier()); + } +} \ No newline at end of file From c0b1c24c19f8b5e88a9258837968998509eb91f8 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 3 Apr 2024 16:13:57 +0200 Subject: [PATCH 166/224] Export facades --- packages/support/src/facades/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/support/src/facades/index.ts diff --git a/packages/support/src/facades/index.ts b/packages/support/src/facades/index.ts new file mode 100644 index 00000000..85fd24c2 --- /dev/null +++ b/packages/support/src/facades/index.ts @@ -0,0 +1,7 @@ +import Container from "./Container"; +import Facade from "./Facade"; + +export { + Container, + Facade +} \ No newline at end of file From 3aaf42ba5211423e9447c68167bf1256789cd70f Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 3 Apr 2024 16:14:13 +0200 Subject: [PATCH 167/224] Add notice for Facade abstraction --- packages/support/NOTICE | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/support/NOTICE b/packages/support/NOTICE index 1aef8b1a..42a8dec5 100644 --- a/packages/support/NOTICE +++ b/packages/support/NOTICE @@ -288,3 +288,35 @@ conditions of the included 3rd party software, in the directory where it has bee LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@aedart/support/facades (Facade) + The Facade abstraction is an adaptation of Laravel Facade class. + + See https://github.com/laravel/framework/blob/master/src/Illuminate/Support/Facades/Facade.php + + License MIT, Copyright (c) Taylor Otwell. + + This part of the NOTICE file corresponds to terms and conditions set by the MIT License + ======================================================================================= + + The MIT License (MIT) + + Copyright (c) Taylor Otwell + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. \ No newline at end of file From f6922a33fa548b496573addb59c455c300efd072 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 3 Apr 2024 16:14:24 +0200 Subject: [PATCH 168/224] Export facade sub-module --- packages/support/package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/support/package.json b/packages/support/package.json index b6d36d8f..22254688 100644 --- a/packages/support/package.json +++ b/packages/support/package.json @@ -48,6 +48,11 @@ "import": "./dist/esm/exceptions.js", "require": "./dist/cjs/exceptions.cjs" }, + "./facades": { + "types": "./dist/types/facades.d.ts", + "import": "./dist/esm/facades.js", + "require": "./dist/cjs/facades.cjs" + }, "./meta": { "types": "./dist/types/meta.d.ts", "import": "./dist/esm/meta.js", From 5e82b15385a3285a46795abf422733e911644387 Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 3 Apr 2024 16:14:38 +0200 Subject: [PATCH 169/224] Mark support/facades as external --- packages/container/rollup.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/container/rollup.config.mjs b/packages/container/rollup.config.mjs index 6df82b12..45ff44b6 100644 --- a/packages/container/rollup.config.mjs +++ b/packages/container/rollup.config.mjs @@ -17,6 +17,7 @@ export default createConfig({ '@aedart/support/container', '@aedart/support/concerns', '@aedart/support/exceptions', + '@aedart/support/facades', '@aedart/support/meta', '@aedart/support/misc', '@aedart/support/mixins', From 742f9395d6fb7b56f6ccd327895f19c14276b51d Mon Sep 17 00:00:00 2001 From: alin Date: Wed, 3 Apr 2024 16:16:03 +0200 Subject: [PATCH 170/224] Fix lint --- packages/support/src/facades/Facade.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/support/src/facades/Facade.ts b/packages/support/src/facades/Facade.ts index 9ef2fb54..da9dee0c 100644 --- a/packages/support/src/facades/Facade.ts +++ b/packages/support/src/facades/Facade.ts @@ -34,7 +34,7 @@ export default abstract class Facade */ static #resolved: Map< Identifier, - any + any /* eslint-disable-line @typescript-eslint/no-explicit-any */ > = new Map(); /** @@ -167,7 +167,9 @@ export default abstract class Facade * * @static */ - public static swap(instance: any): void + public static swap( + instance: any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): void { const identifier = this.getIdentifier(); From 2683118efd12a45a3db6a31b7962659adce563f5 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 09:37:33 +0200 Subject: [PATCH 171/224] Improve descriptions --- packages/support/src/facades/Facade.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/support/src/facades/Facade.ts b/packages/support/src/facades/Facade.ts index da9dee0c..fb4a89bd 100644 --- a/packages/support/src/facades/Facade.ts +++ b/packages/support/src/facades/Facade.ts @@ -58,7 +58,7 @@ export default abstract class Facade } /** - * Returns identifier to be used for resolving facade's instance + * Returns identifier to be used for resolving facade's underlying object instance * * @return {Identifier} * @@ -72,8 +72,10 @@ export default abstract class Facade } /** - * Obtain the facade's underlying resolved object instance, or object "spy" + * Obtain the underlying object instance, or a "spy" (for testing) * + * @see spy + * * @return {any} * * @throws {NotFoundException} From 4da4b74bcc8c36f22753d5474e9e0019319d9ff4 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 10:00:26 +0200 Subject: [PATCH 172/224] Add resolveIdentifier() method This is slightly more convenient to invoke, rather than having to manually get the identifier. --- packages/support/src/facades/Container.ts | 2 +- packages/support/src/facades/Facade.ts | 36 +++++++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/support/src/facades/Container.ts b/packages/support/src/facades/Container.ts index ebd11232..9426ae15 100644 --- a/packages/support/src/facades/Container.ts +++ b/packages/support/src/facades/Container.ts @@ -16,6 +16,6 @@ export default class Container extends Facade public static obtain() { - return this.resolve(this.getIdentifier()); + return this.resolveIdentifier(); } } \ No newline at end of file diff --git a/packages/support/src/facades/Facade.ts b/packages/support/src/facades/Facade.ts index fb4a89bd..55485920 100644 --- a/packages/support/src/facades/Facade.ts +++ b/packages/support/src/facades/Facade.ts @@ -88,6 +88,9 @@ export default abstract class Facade */ public static obtain() { + // Use this method to resolve the binding from the service container. + // E.g. return this.resolveIdentifier(); + throw new LogicalError('Facade does not implement the get() method'); } @@ -191,7 +194,7 @@ export default abstract class Facade * * @static */ - public static setContainer(container: Container | undefined): Facade + public static setContainer(container: Container | undefined): typeof Facade { this.#container = container; @@ -229,7 +232,7 @@ export default abstract class Facade * * @static */ - public static forgetContainer(): Facade + public static forgetContainer(): typeof Facade { this.#container = undefined; @@ -289,7 +292,34 @@ export default abstract class Facade } /** - * Get the facade's underlying object instance or object spy + * Resolves the facade's underlying object instance from the service container. + * + * @see resolve + * + * @template T = any + * + * @return {T} + * + * @throws {NotFoundException} + * @throws {ContainerException} + * @throws {LogicalError} + * + * @protected + * @static + */ + protected static resolveIdentifier< + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(): T + { + return this.resolve(this.getIdentifier()); + } + + /** + * Resolves the facade's underlying object instance from the service container, + * which matches given identifier. + * + * **Note**: _If a "spy" has been registered for given identifier, then that spy + * object is returned instead._ * * @template T = any * From 0f7cb78c4f6fd91dbcc021054141a12c5f710005 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 10:12:57 +0200 Subject: [PATCH 173/224] Fix Abstract Class Error argument --- packages/support/src/facades/Facade.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/facades/Facade.ts b/packages/support/src/facades/Facade.ts index 55485920..ff9a46ba 100644 --- a/packages/support/src/facades/Facade.ts +++ b/packages/support/src/facades/Facade.ts @@ -54,7 +54,7 @@ export default abstract class Facade */ protected constructor() { /* @ts-expect-error TS2345 Facade constructor is abstract */ - throw new AbstractClassError(this); + throw new AbstractClassError(this.constructor); } /** From 91f582a359ec666af86d48d8e28bd6c2ade11c64 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 10:23:37 +0200 Subject: [PATCH 174/224] Rename destroyFacades() to destroy() --- packages/support/src/facades/Facade.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/support/src/facades/Facade.ts b/packages/support/src/facades/Facade.ts index ff9a46ba..04626330 100644 --- a/packages/support/src/facades/Facade.ts +++ b/packages/support/src/facades/Facade.ts @@ -280,11 +280,11 @@ export default abstract class Facade } /** - * Destroy all resolved facade instances and forget the service container + * Clears all resolved instances, service container and evt. spies. * * @return {void} */ - public static destroyFacades(): void + public static destroy(): void { this.forgetAllSpies(); this.forgetAllResolved(); From a56230783a3a554f4bb49cd156204b644b71fbf6 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 10:34:56 +0200 Subject: [PATCH 175/224] Fix private properties are not inherited by child classes Ups - keep forgetting that private properties are not part of the damn prototype inheritance chain. --- packages/support/src/facades/Facade.ts | 42 +++++++++++++------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/support/src/facades/Facade.ts b/packages/support/src/facades/Facade.ts index 04626330..eb0d95e2 100644 --- a/packages/support/src/facades/Facade.ts +++ b/packages/support/src/facades/Facade.ts @@ -19,20 +19,20 @@ export default abstract class Facade * * @type {Container|undefined} * - * @private + * @protected * @static */ - static #container: Container | undefined = undefined; + protected static container: Container | undefined = undefined; /** * Resolved instances * * @type {Map} * - * @private + * @protected * @static */ - static #resolved: Map< + protected static resolved: Map< Identifier, any /* eslint-disable-line @typescript-eslint/no-explicit-any */ > = new Map(); @@ -42,10 +42,10 @@ export default abstract class Facade * * @type {Set} * - * @private + * @protected * @static */ - static #spies: Set = new Set(); + protected static spies: Set = new Set(); /** * Facade Constructor @@ -112,7 +112,7 @@ export default abstract class Facade this.swap(spy); - this.#spies.add(identifier); + this.spies.add(identifier); return spy; } @@ -126,7 +126,7 @@ export default abstract class Facade */ public static isSpy(): boolean { - return this.#spies.has(this.getIdentifier()); + return this.spies.has(this.getIdentifier()); } /** @@ -144,7 +144,7 @@ export default abstract class Facade const identifier = this.getIdentifier(); return this.forgetResolved(identifier) - && this.#spies.delete(identifier); + && this.spies.delete(identifier); } /** @@ -156,11 +156,11 @@ export default abstract class Facade */ public static forgetAllSpies(): void { - for (const identifier of this.#spies) { + for (const identifier of this.spies) { this.forgetResolved(identifier); } - this.#spies.clear(); + this.spies.clear(); } /** @@ -178,7 +178,7 @@ export default abstract class Facade { const identifier = this.getIdentifier(); - this.#resolved.set(identifier, instance); + this.resolved.set(identifier, instance); if (this.hasContainer()) { (this.getContainer() as Container).instance(identifier, instance); @@ -196,7 +196,7 @@ export default abstract class Facade */ public static setContainer(container: Container | undefined): typeof Facade { - this.#container = container; + this.container = container; return this; } @@ -210,7 +210,7 @@ export default abstract class Facade */ public static getContainer(): Container | undefined { - return this.#container; + return this.container; } /** @@ -222,7 +222,7 @@ export default abstract class Facade */ public static hasContainer(): boolean { - return isset(this.#container); + return isset(this.container); } /** @@ -234,7 +234,7 @@ export default abstract class Facade */ public static forgetContainer(): typeof Facade { - this.#container = undefined; + this.container = undefined; return this; } @@ -250,7 +250,7 @@ export default abstract class Facade */ public static hasResolved(identifier: Identifier): boolean { - return this.#resolved.has(identifier); + return this.resolved.has(identifier); } /** @@ -264,7 +264,7 @@ export default abstract class Facade */ public static forgetResolved(identifier: Identifier): boolean { - return this.#resolved.delete(identifier); + return this.resolved.delete(identifier); } /** @@ -276,7 +276,7 @@ export default abstract class Facade */ public static forgetAllResolved(): void { - this.#resolved.clear(); + this.resolved.clear(); } /** @@ -344,11 +344,11 @@ export default abstract class Facade } if (this.hasResolved(identifier)) { - return this.#resolved.get(identifier) as T; + return this.resolved.get(identifier) as T; } const resolved = (this.getContainer() as Container).make(identifier); - this.#resolved.set(identifier, resolved); + this.resolved.set(identifier, resolved); return resolved; } From a9c428e21d83ac41eca537067d7a3ca2c1ca743b Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 12:51:49 +0200 Subject: [PATCH 176/224] Init contracts/support/facades submodule --- aliases.js | 1 + packages/contracts/package.json | 5 +++++ packages/contracts/rollup.config.mjs | 1 + packages/support/rollup.config.mjs | 1 + 4 files changed, 8 insertions(+) diff --git a/aliases.js b/aliases.js index 74676665..2034e3f5 100644 --- a/aliases.js +++ b/aliases.js @@ -26,6 +26,7 @@ module.exports = { '@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/facades': path.resolve(__dirname, './packages/contracts/support/facades'), '@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'), diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 76d18e93..be0397e2 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -56,6 +56,11 @@ "import": "./dist/esm/support/exceptions.js", "require": "./dist/cjs/support/exceptions.cjs" }, + "./support/facades": { + "types": "./dist/types/support/facades.d.ts", + "import": "./dist/esm/support/facades.js", + "require": "./dist/cjs/support/facades.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 c58efd45..b35ca141 100644 --- a/packages/contracts/rollup.config.mjs +++ b/packages/contracts/rollup.config.mjs @@ -8,6 +8,7 @@ export default createConfig({ '@aedart/contracts/support/arrays', '@aedart/contracts/support/concerns', '@aedart/contracts/support/exceptions', + '@aedart/contracts/support/facades', '@aedart/contracts/support/meta', '@aedart/contracts/support/mixins', '@aedart/contracts/support/reflections', diff --git a/packages/support/rollup.config.mjs b/packages/support/rollup.config.mjs index 75707c34..8b8428e8 100644 --- a/packages/support/rollup.config.mjs +++ b/packages/support/rollup.config.mjs @@ -9,6 +9,7 @@ export default createConfig({ '@aedart/contracts/support/arrays', '@aedart/contracts/support/concerns', '@aedart/contracts/support/exceptions', + '@aedart/contracts/support/facades', '@aedart/contracts/support/meta', '@aedart/contracts/support/mixins', '@aedart/contracts/support/objects', From ec0e814fe552476a65e84e2f54a864c0753ca8ad Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 12:52:09 +0200 Subject: [PATCH 177/224] Add SpyFactoryCallback type alias --- packages/contracts/src/support/facades/types.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/contracts/src/support/facades/types.ts diff --git a/packages/contracts/src/support/facades/types.ts b/packages/contracts/src/support/facades/types.ts new file mode 100644 index 00000000..21ebcf58 --- /dev/null +++ b/packages/contracts/src/support/facades/types.ts @@ -0,0 +1,8 @@ +import { Container, Identifier } from "@aedart/contracts/container"; + +/** + * Callback used to create a "spy" (e.g. mocked object), for testing purposes. + */ +export type SpyFactoryCallback< + T = any +> = (container: Container, identifier: Identifier) => T; \ No newline at end of file From 8e3d53db1d7649401326e4e49246ffb2b5d682ad Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 12:52:30 +0200 Subject: [PATCH 178/224] Export facade related types aliases --- packages/contracts/src/support/facades/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/contracts/src/support/facades/index.ts diff --git a/packages/contracts/src/support/facades/index.ts b/packages/contracts/src/support/facades/index.ts new file mode 100644 index 00000000..91bbcb7b --- /dev/null +++ b/packages/contracts/src/support/facades/index.ts @@ -0,0 +1,8 @@ +/** + * Support Facades identifier + * + * @type {Symbol} + */ +export const SUPPORT_FACADES: unique symbol = Symbol('@aedart/contracts/support/facades'); + +export type * from './types'; \ No newline at end of file From f004bfefd16a5bf261491f9dde17356112e1e0df Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 12:53:53 +0200 Subject: [PATCH 179/224] Ignore any type --- packages/contracts/src/support/facades/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/support/facades/types.ts b/packages/contracts/src/support/facades/types.ts index 21ebcf58..5d10d9cd 100644 --- a/packages/contracts/src/support/facades/types.ts +++ b/packages/contracts/src/support/facades/types.ts @@ -4,5 +4,5 @@ import { Container, Identifier } from "@aedart/contracts/container"; * Callback used to create a "spy" (e.g. mocked object), for testing purposes. */ export type SpyFactoryCallback< - T = any + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ > = (container: Container, identifier: Identifier) => T; \ No newline at end of file From 2517951a903f164fe2bea7683cc6d0c42f4e149e Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 13:00:07 +0200 Subject: [PATCH 180/224] Replace spy callback type with SpyFactoryCallback type alias --- packages/support/src/facades/Facade.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/support/src/facades/Facade.ts b/packages/support/src/facades/Facade.ts index eb0d95e2..c91651c7 100644 --- a/packages/support/src/facades/Facade.ts +++ b/packages/support/src/facades/Facade.ts @@ -1,5 +1,5 @@ import type { Container, Identifier } from "@aedart/contracts/container"; -import type { Callback } from "@aedart/contracts"; +import type { SpyFactoryCallback } from "@aedart/contracts/support/facades"; import { isset } from "@aedart/support/misc"; import { AbstractClassError, LogicalError } from "@aedart/support/exceptions"; @@ -97,18 +97,22 @@ export default abstract class Facade /** * Register a "spy" (e.g. object mock) for this facade's identifier * - * @param {Callback} callback Callback to be used for creating some kind of object spy + * @template T = any + * + * @param {SpyFactoryCallback} callback Callback to be used for creating some kind of object spy * or mock, with appropriate configuration and expectations for testing * purposes. * - * @return {ReturnType} + * @return {T} Object instance (spy / object mock) to be registered in service container. * * @static */ - public static spy(callback: Callback): ReturnType + public static spy< + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(callback: SpyFactoryCallback): T { const identifier = this.getIdentifier(); - const spy = callback(identifier); + const spy = callback(this.getContainer() as Container, identifier); this.swap(spy); From 748170b81511baea590648e1531aa143e158b414 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 13:04:03 +0200 Subject: [PATCH 181/224] Improve JSDoc for Container Facade --- packages/support/src/facades/Container.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/support/src/facades/Container.ts b/packages/support/src/facades/Container.ts index 9426ae15..4e8aa85c 100644 --- a/packages/support/src/facades/Container.ts +++ b/packages/support/src/facades/Container.ts @@ -4,8 +4,6 @@ import Facade from "./Facade"; /** * Container Facade - * - * @extends Facade */ export default class Container extends Facade { @@ -14,6 +12,11 @@ export default class Container extends Facade return CONTAINER; } + /** + * @inheritDoc + * + * @return {import('@aedart/contracts/container').Container} + */ public static obtain() { return this.resolveIdentifier(); From 6f1d1d7bb137dba752b27e293e7d93e72bde2b50 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 13:04:26 +0200 Subject: [PATCH 182/224] Add tests for Facade abstraction --- .../packages/support/facades/Facade.test.js | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 tests/browser/packages/support/facades/Facade.test.js diff --git a/tests/browser/packages/support/facades/Facade.test.js b/tests/browser/packages/support/facades/Facade.test.js new file mode 100644 index 00000000..0e7d866c --- /dev/null +++ b/tests/browser/packages/support/facades/Facade.test.js @@ -0,0 +1,216 @@ +import { Facade, Container as ContainerFacade } from "@aedart/support/facades"; +import { CONTAINER } from "@aedart/contracts/container"; +import { Container } from "@aedart/container"; +import { AbstractClassError, LogicalError } from "@aedart/support/exceptions"; + +describe('@aedart/support/facades', () => { + describe('Facade', () => { + + afterEach(() => { + Facade.destroy(); + }); + + it('fails when creating a new instance', () => { + const callback = () => { + return new ContainerFacade(); + } + + expect(callback) + .toThrowError(AbstractClassError); + }); + + it('fails when getIdentifier() not implemented', () => { + + class MyFacade extends Facade {} + + const callback = () => { + return MyFacade.getIdentifier(); + } + + expect(callback) + .toThrowError(LogicalError); + }); + + it('fails when obtain() not implemented', () => { + + class MyFacade extends Facade {} + + const callback = () => { + return MyFacade.obtain(); + } + + expect(callback) + .toThrowError(LogicalError); + }); + + it('can set and get service container', () => { + + const container = new Container(); + + Facade.setContainer(container); + + // --------------------------------------------------------------- // + + const result = Facade.getContainer(); + + expect(Facade.hasContainer()) + .withContext('Service Container not available in Facade') + .toBeTrue(); + + expect(result) + .withContext('Incorrect Service Container instance') + .toBe(container); + + expect(ContainerFacade.getContainer()) + .withContext('Service Container instance not available in concrete facade class') + .toBe(container); + }); + + it('can forget service container', () => { + + const container = new Container(); + + Facade.setContainer(container); + + // --------------------------------------------------------------- // + + Facade.forgetContainer() + + expect(Facade.hasContainer()) + .withContext('Service Container still available in Facade') + .toBeFalse(); + }); + + it('returns identifier, when defined in concrete facade class', () => { + class MyFacade extends Facade { + static getIdentifier() + { + return 'my_identifier'; + } + } + + // --------------------------------------------------------------- // + + const result = MyFacade.getIdentifier(); + + expect(result) + .toBe('my_identifier'); + }); + + it('can obtain underlying instance', () => { + const container = new Container(); + container.instance(CONTAINER, container); + + Facade.setContainer(container); + + // --------------------------------------------------------------- // + + const result = ContainerFacade.obtain(); + expect(result) + .toBe(container); + + expect(Facade.hasResolved(CONTAINER)) + .withContext('Facade does not have instance marked as resolved') + .toBeTrue(); + }); + + it('can forget resolved underlying instance', () => { + const container = new Container(); + container.instance(CONTAINER, container); + + Facade.setContainer(container); + + // --------------------------------------------------------------- // + + const result = ContainerFacade.obtain(); + expect(result) + .toBe(container); + + expect(Facade.forgetResolved(CONTAINER)) + .withContext('Unable to forget resoled') + .toBeTrue(); + + expect(Facade.hasResolved(CONTAINER)) + .withContext('Facade should NOT have instance marked as resolved') + .toBeFalse(); + }); + + it('can register spy for concrete facade', () => { + const container = new Container(); + container.instance(CONTAINER, container); + Facade.setContainer(container); + + // --------------------------------------------------------------- // + + class MyFacade extends Facade { + static getIdentifier() + { + return 'my_identifier'; + } + + /** + * @return {Foo} + */ + static obtain() { + return this.resolveIdentifier(); + } + } + + class Foo { + bar() { + return 'bar'; + } + } + + container.singleton('my_identifier', Foo); + + // --------------------------------------------------------------- // + + let spy = null; + const registered = MyFacade.spy((container, identifier) => { + const obj = container.get(identifier); + + spy = spyOn(obj, 'bar') + .and + .returnValue('done'); + + // NOTE: return entire object in this case - not just the mocked + // function... + return obj; + }); + + // --------------------------------------------------------------- // + + expect(MyFacade.isSpy()) + .withContext('Concrete Facade should be marked as a spy') + .toBeTrue(); + + expect(ContainerFacade.isSpy()) + .withContext('Other concrete Facade should NOT be marked as a spy') + .toBeFalse(); + + const resolved = MyFacade.obtain(); + expect(resolved) + .withContext('Resolved instance should be a spy object') + .toBe(registered); + + const result = resolved.bar(); + expect(result) + .withContext('Spy does not appear to have worked...!') + .toBe('done'); + + expect(spy) + .withContext('Spy not invoked!') + .toHaveBeenCalled(); + + // --------------------------------------------------------------- // + + expect(MyFacade.forgetSpy()) + .withContext('Unable to forget spy') + .toBeTrue(); + expect(MyFacade.isSpy()) + .withContext('Concrete Facade should NOT LONGER be marked as a spy') + .toBeFalse(); + }); + }); +}); \ No newline at end of file From 31dd7b864e922fe89d7b5125a8cd279c31504f31 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 13:06:01 +0200 Subject: [PATCH 183/224] Change release notes --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94c80743..2d2bdd3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Service Container package (`@aedart/container`). +* `Facade` abstraction, in `@aedart/support/facades`. * `DEPENDENCIES` symbol and `Identifier` type in `@aedart/contracts/container`. * `dependsOn()`, `dependencies()`, `hasDependencies()`, and `getDependencies()`, in `@aedart/support/container`. * `isBindingIdentifier`, in `@aedart/support/container`. From 3ad91ce238c3698cf70ab1f41d5150472b98d1eb Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 13:31:42 +0200 Subject: [PATCH 184/224] Change from private to protected visibility for class properties --- packages/container/src/BindingEntry.ts | 44 +++++++++++-------- packages/container/src/Container.ts | 10 ++--- packages/support/src/ArbitraryData.ts | 16 +++---- packages/support/src/CallbackWrapper.ts | 24 +++++----- packages/support/src/arrays/merge/Merger.ts | 10 ++--- .../support/src/concerns/AbstractConcern.ts | 12 ++--- .../support/src/concerns/ConcernsContainer.ts | 12 ++--- .../support/src/concerns/ConcernsInjector.ts | 8 ++-- packages/support/src/concerns/Repository.ts | 16 +++---- .../concerns/exceptions/AliasConflictError.ts | 36 +++++++-------- .../exceptions/AlreadyRegisteredError.ts | 21 ++++++--- .../src/concerns/exceptions/ConcernError.ts | 11 ++--- .../src/concerns/exceptions/InjectionError.ts | 11 ++--- .../concerns/exceptions/UnsafeAliasError.ts | 22 +++++----- packages/support/src/meta/MetaRepository.ts | 9 ++-- packages/support/src/mixins/Builder.ts | 13 +++--- packages/support/src/objects/ObjectId.ts | 22 ++++++---- packages/support/src/objects/merge/Merger.ts | 23 +++++----- 18 files changed, 174 insertions(+), 146 deletions(-) diff --git a/packages/container/src/BindingEntry.ts b/packages/container/src/BindingEntry.ts index 2d247769..aecd2876 100644 --- a/packages/container/src/BindingEntry.ts +++ b/packages/container/src/BindingEntry.ts @@ -20,8 +20,10 @@ export default class BindingEntry< * @type {Identifier} * * @readonly + * + * @protected */ - readonly #identifier: Identifier; + protected readonly _identifier: Identifier; /** * The bound value to be resolved by a service container @@ -31,8 +33,10 @@ export default class BindingEntry< * @type {FactoryCallback | Constructor} * * @readonly + * + * @protected */ - readonly #value: FactoryCallback | Constructor; + protected readonly _value: FactoryCallback | Constructor; /** * Shared state of resolved value @@ -41,26 +45,28 @@ export default class BindingEntry< * value as a singleton. * * @readonly + * + * @protected */ - readonly #shared: boolean; + protected readonly _shared: boolean; /** * State, whether value is a factory callback or not * * @type {boolean|null} * - * @private + * @protected */ - #isFactoryCallback: boolean|null = null; + protected _isFactoryCallback: boolean|null = null; /** * State, whether value is a constructor or not * * @type {boolean|null} * - * @private + * @protected */ - #isConstructor: boolean|null = null; + _isConstructor: boolean|null = null; /** * Create new Binding Entry instance @@ -79,9 +85,9 @@ export default class BindingEntry< throw new TypeError(`Invalid binding identifier: ${typeof identifier} is not supported`, { cause: { identifier: identifier, value: value } }); } - this.#identifier = identifier; - this.#value = value; - this.#shared = shared; + this._identifier = identifier; + this._value = value; + this._shared = shared; this.resolveIsConstructorOrFactoryCallback(); } @@ -95,7 +101,7 @@ export default class BindingEntry< */ get identifier(): Identifier { - return this.#identifier; + return this._identifier; } /** @@ -109,7 +115,7 @@ export default class BindingEntry< */ get value(): FactoryCallback | Constructor { - return this.#value; + return this._value; } /** @@ -122,7 +128,7 @@ export default class BindingEntry< */ get shared(): boolean { - return this.#shared; + return this._shared; } /** @@ -132,7 +138,7 @@ export default class BindingEntry< */ isFactoryCallback(): boolean { - return this.#isFactoryCallback as boolean; + return this._isFactoryCallback as boolean; } /** @@ -142,7 +148,7 @@ export default class BindingEntry< */ isConstructor(): boolean { - return this.#isConstructor as boolean; + return this._isConstructor as boolean; } /** @@ -154,11 +160,11 @@ export default class BindingEntry< */ protected resolveIsConstructorOrFactoryCallback(): void { - this.#isConstructor = isConstructor(this.#value); - this.#isFactoryCallback = !this.#isConstructor; + this._isConstructor = isConstructor(this._value); + this._isFactoryCallback = !this._isConstructor; - if (this.#isConstructor === false && this.#isFactoryCallback === false) { - throw new TypeError('Binding value must either be a valid constructor or factory callback', { cause: { identifier: this.#identifier, value: this.#value } }); + if (!this._isConstructor && !this._isFactoryCallback) { + throw new TypeError('Binding value must either be a valid constructor or factory callback', { cause: { identifier: this._identifier, value: this._value } }); } } } \ No newline at end of file diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index fa754b10..0fd7e8b6 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -50,11 +50,11 @@ export default class Container implements ServiceContainerContract * * @type {ServiceContainerContract|null} * - * @private + * @protected * * @static */ - static #instance: ServiceContainerContract | null = null; + protected static _instance: ServiceContainerContract | null = null; /** * Registered bindings @@ -144,11 +144,11 @@ export default class Container implements ServiceContainerContract */ public static getInstance(): ServiceContainerContract { - if (this.#instance === null) { + if (this._instance === null) { this.setInstance(new this()); } - return this.#instance as ServiceContainerContract; + return this._instance as ServiceContainerContract; } /** @@ -160,7 +160,7 @@ export default class Container implements ServiceContainerContract */ public static setInstance(container: ServiceContainerContract | null = null): ServiceContainerContract | null { - return this.#instance = container; + return this._instance = container; } /** diff --git a/packages/support/src/ArbitraryData.ts b/packages/support/src/ArbitraryData.ts index 812c337d..6e472a86 100644 --- a/packages/support/src/ArbitraryData.ts +++ b/packages/support/src/ArbitraryData.ts @@ -17,9 +17,9 @@ export default class ArbitraryData extends AbstractConcern implements HasArbitra * * @type {Record} * - * @private + * @protected */ - #data: Record = {}; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + protected _data: Record = {}; /* eslint-disable-line @typescript-eslint/no-explicit-any */ /** * Set value for key @@ -31,7 +31,7 @@ export default class ArbitraryData extends AbstractConcern implements HasArbitra */ set(key: Key, value: any): this /* eslint-disable-line @typescript-eslint/no-explicit-any */ { - set(this.#data, key, value); + set(this._data, key, value); return this.concernOwner as this; } @@ -49,7 +49,7 @@ export default class ArbitraryData extends AbstractConcern implements HasArbitra */ get(key: Key, defaultValue?: D): T | D { - return get(this.#data, key, defaultValue); + return get(this._data, key, defaultValue); } /** @@ -61,7 +61,7 @@ export default class ArbitraryData extends AbstractConcern implements HasArbitra */ has(key: Key): boolean { - return has(this.#data, key); + return has(this._data, key); } /** @@ -73,7 +73,7 @@ export default class ArbitraryData extends AbstractConcern implements HasArbitra */ forget(key: Key): boolean { - return forget(this.#data, key); + return forget(this._data, key); } /** @@ -84,7 +84,7 @@ export default class ArbitraryData extends AbstractConcern implements HasArbitra all(): Record /* eslint-disable-line @typescript-eslint/no-explicit-any */ { // Returns a copy of the arbitrary data record - return merge(this.#data); + return merge(this._data); } /** @@ -94,6 +94,6 @@ export default class ArbitraryData extends AbstractConcern implements HasArbitra */ flush(): void { - this.#data = {}; + this._data = {}; } } \ No newline at end of file diff --git a/packages/support/src/CallbackWrapper.ts b/packages/support/src/CallbackWrapper.ts index c176c445..942c15a6 100644 --- a/packages/support/src/CallbackWrapper.ts +++ b/packages/support/src/CallbackWrapper.ts @@ -88,10 +88,10 @@ export default class CallbackWrapper implements CallbackWrapperInterface * * @type {Callback} * - * @private + * @protected * @readonly */ - readonly #callback: Callback; + protected readonly _callback: Callback; /** * "This" value that callback is bound to @@ -99,9 +99,9 @@ export default class CallbackWrapper implements CallbackWrapperInterface * @type {object | undefined} * * @readonly - * @private + * @protected */ - #binding: object | undefined = undefined; + protected _binding: object | undefined = undefined; /** * Arguments to be passed on to the callback @@ -109,9 +109,9 @@ export default class CallbackWrapper implements CallbackWrapperInterface * * @type {any[]} * - * @private + * @protected */ - #arguments: any[] = []; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + protected _arguments: any[] = []; /* eslint-disable-line @typescript-eslint/no-explicit-any */ /** * Create a new Callback Wrapper instance @@ -129,7 +129,7 @@ export default class CallbackWrapper implements CallbackWrapperInterface throw new TypeError('Argument must be a valid callable function'); } - this.#callback = callback; + this._callback = callback; this.with(...args); } @@ -180,7 +180,7 @@ export default class CallbackWrapper implements CallbackWrapperInterface */ public get callback(): Callback { - return this.#callback; + return this._callback; } /** @@ -192,7 +192,7 @@ export default class CallbackWrapper implements CallbackWrapperInterface */ public get binding(): object | undefined { - return this.#binding; + return this._binding; } /** @@ -203,7 +203,7 @@ export default class CallbackWrapper implements CallbackWrapperInterface */ public set arguments(args: any[]) /* eslint-disable-line @typescript-eslint/no-explicit-any */ { - this.#arguments = args; + this._arguments = args; } /** @@ -214,7 +214,7 @@ export default class CallbackWrapper implements CallbackWrapperInterface */ public get arguments(): any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ { - return this.#arguments; + return this._arguments; } /** @@ -253,7 +253,7 @@ export default class CallbackWrapper implements CallbackWrapperInterface */ public bind(thisArg: object): this { - this.#binding = thisArg; + this._binding = thisArg; return this; } diff --git a/packages/support/src/arrays/merge/Merger.ts b/packages/support/src/arrays/merge/Merger.ts index e9733104..067b07fc 100644 --- a/packages/support/src/arrays/merge/Merger.ts +++ b/packages/support/src/arrays/merge/Merger.ts @@ -17,9 +17,9 @@ export default class Merger implements ArrayMerger * * @type {Readonly} * - * @private + * @protected */ - #options: Readonly; + protected _options: Readonly; /** * Create new Array Merger instance @@ -28,7 +28,7 @@ export default class Merger implements ArrayMerger */ public constructor(options?: ArrayMergeCallback | ArrayMergeOptions) { // @ts-expect-error Need to init options, however they are resolved via "using". - this.#options = null; + this._options = null; this.using(options); } @@ -44,7 +44,7 @@ export default class Merger implements ArrayMerger */ using(options?: ArrayMergeCallback | ArrayMergeOptions): this { - this.#options = this.resolveOptions(options); + this._options = this.resolveOptions(options); return this; } @@ -56,7 +56,7 @@ export default class Merger implements ArrayMerger */ public get options(): Readonly { - return this.#options; + return this._options; } /** diff --git a/packages/support/src/concerns/AbstractConcern.ts b/packages/support/src/concerns/AbstractConcern.ts index aa055489..f086c404 100644 --- a/packages/support/src/concerns/AbstractConcern.ts +++ b/packages/support/src/concerns/AbstractConcern.ts @@ -20,12 +20,12 @@ export default abstract class AbstractConcern implements Concern * The owner class instance this concern is injected into, * or `this` concern instance. * - * @readonly - * @private - * * @type {object} + * + * @readonly + * @protected */ - readonly #concernOwner: object; + protected readonly _concernOwner: object; /** * Creates a new concern instance @@ -42,7 +42,7 @@ export default abstract class AbstractConcern implements Concern throw new AbstractClassError(AbstractConcern); } - this.#concernOwner = owner || this; + this._concernOwner = owner || this; } /** @@ -55,7 +55,7 @@ export default abstract class AbstractConcern implements Concern */ public get concernOwner(): object { - return this.#concernOwner; + return this._concernOwner; } /** diff --git a/packages/support/src/concerns/ConcernsContainer.ts b/packages/support/src/concerns/ConcernsContainer.ts index e263ffe3..a287d9b8 100644 --- a/packages/support/src/concerns/ConcernsContainer.ts +++ b/packages/support/src/concerns/ConcernsContainer.ts @@ -29,12 +29,12 @@ export default class ConcernsContainer implements Container /** * The concerns owner of this container * - * @private - * @readonly - * * @type {Owner} + * + * @protected + * @readonly */ - readonly #owner: Owner; + protected readonly _owner: Owner; /** * Create a new Concerns Container instance @@ -43,7 +43,7 @@ export default class ConcernsContainer implements Container * @param {ConcernConstructor[]} concerns */ public constructor(owner: Owner, concerns: ConcernConstructor[]) { - this.#owner = owner; + this._owner = owner; this.map = new Map(); for(const concern of concerns) { @@ -72,7 +72,7 @@ export default class ConcernsContainer implements Container */ public get owner(): Owner { - return this.#owner; + return this._owner; } /** diff --git a/packages/support/src/concerns/ConcernsInjector.ts b/packages/support/src/concerns/ConcernsInjector.ts index 3d093fe1..070cfef7 100644 --- a/packages/support/src/concerns/ConcernsInjector.ts +++ b/packages/support/src/concerns/ConcernsInjector.ts @@ -58,9 +58,9 @@ export default class ConcernsInjector implements Injector * @template T = object * @type {T} * - * @private + * @protected */ - readonly #target: T; + protected readonly _target: T; /** * Concern Configuration Factory @@ -106,7 +106,7 @@ export default class ConcernsInjector implements Injector repository?: DescriptorsRepository ) { - this.#target = target; + this._target = target; this.configFactory = configFactory || new ConfigurationFactory(); this.descriptorFactory = descriptorFactory || new DescriptorFactory(); this.repository = repository || new Repository(); @@ -121,7 +121,7 @@ export default class ConcernsInjector implements Injector */ public get target(): T { - return this.#target; + return this._target; } /** diff --git a/packages/support/src/concerns/Repository.ts b/packages/support/src/concerns/Repository.ts index 412e93e3..844841f9 100644 --- a/packages/support/src/concerns/Repository.ts +++ b/packages/support/src/concerns/Repository.ts @@ -14,9 +14,9 @@ export default class Repository implements DescriptorsRepository * * @type {WeakMap>} * - * @private + * @protected */ - #store: WeakMap< + protected _store: WeakMap< ConstructorLike | UsesConcerns | ConcernConstructor, Record >; @@ -25,7 +25,7 @@ export default class Repository implements DescriptorsRepository * Create new Descriptors instance */ constructor() { - this.#store = new WeakMap(); + this._store = new WeakMap(); } /** @@ -43,13 +43,13 @@ export default class Repository implements DescriptorsRepository cache: boolean = false ): Record { - if (!force && this.#store.has(target)) { - return this.#store.get(target) as 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); + this._store.set(target, descriptors); } return descriptors; @@ -101,7 +101,7 @@ export default class Repository implements DescriptorsRepository */ public forget(target: ConstructorLike | UsesConcerns | ConcernConstructor): boolean { - return this.#store.delete(target); + return this._store.delete(target); } /** @@ -111,7 +111,7 @@ export default class Repository implements DescriptorsRepository */ public clear(): this { - this.#store = new WeakMap(); + this._store = new WeakMap(); return this; } diff --git a/packages/support/src/concerns/exceptions/AliasConflictError.ts b/packages/support/src/concerns/exceptions/AliasConflictError.ts index 33a2f4b8..cd4da577 100644 --- a/packages/support/src/concerns/exceptions/AliasConflictError.ts +++ b/packages/support/src/concerns/exceptions/AliasConflictError.ts @@ -15,32 +15,32 @@ export default class AliasConflictError extends InjectionError implements AliasC * The requested alias that conflicts with another alias * of the same name. * - * @readonly - * @private - * * @type {Alias} + * + * @readonly + * @protected */ - readonly #alias: Alias; + protected readonly _alias: Alias; /** * the property key that the conflicting alias points to * - * @readonly - * @private - * * @type {PropertyKey} + * + * @readonly + * @protected */ - readonly #key: PropertyKey; + readonly _key: PropertyKey; /** * The source class (e.g. parent class) that defines that originally defined the alias * - * @readonly - * @private - * * @type {ConstructorLike | UsesConcerns} + * + * @readonly + * @protected */ - readonly #source: ConstructorLike | UsesConcerns; + protected readonly _source: ConstructorLike | UsesConcerns; /** * Create a new Alias Conflict Error instance @@ -67,9 +67,9 @@ export default class AliasConflictError extends InjectionError implements AliasC configureCustomError(this); - this.#alias = alias; - this.#key = key; - this.#source = source; + this._alias = alias; + this._key = key; + this._source = source; // Force set the properties in the cause (this.cause as Record).alias = alias; @@ -86,7 +86,7 @@ export default class AliasConflictError extends InjectionError implements AliasC */ get alias(): Alias { - return this.#alias; + return this._alias; } /** @@ -98,7 +98,7 @@ export default class AliasConflictError extends InjectionError implements AliasC */ get key(): PropertyKey { - return this.#key; + return this._key; } /** @@ -110,6 +110,6 @@ export default class AliasConflictError extends InjectionError implements AliasC */ get source(): ConstructorLike | UsesConcerns { - return this.#source; + return this._source; } } \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts b/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts index 599c2026..c74e897b 100644 --- a/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts +++ b/packages/support/src/concerns/exceptions/AlreadyRegisteredError.ts @@ -19,13 +19,22 @@ export default class AlreadyRegisteredError extends InjectionError implements Al * The source, e.g. a parent class, in which a concern class * was already registered. * - * @readonly - * @private - * * @type {ConstructorLike|UsesConcerns} + * + * @readonly + * @protected */ - readonly #source: ConstructorLike | UsesConcerns; + protected readonly _source: ConstructorLike | UsesConcerns; + /** + * Create a new "already registered" error instance + * + * @param {ConstructorLike | UsesConcerns} target + * @param {ConcernConstructor} concern + * @param {ConstructorLike | UsesConcerns} source + * @param {string} [message] + * @param {ErrorOptions} [options] + */ constructor( target: ConstructorLike | UsesConcerns, concern: ConcernConstructor, @@ -41,7 +50,7 @@ export default class AlreadyRegisteredError extends InjectionError implements Al configureCustomError(this); - this.#source = source; + this._source = source; // Force set the source in the cause (this.cause as Record).source = source; @@ -57,6 +66,6 @@ export default class AlreadyRegisteredError extends InjectionError implements Al */ get source(): ConstructorLike | UsesConcerns { - return this.#source; + return this._source; } } \ 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 0d69042a..1b54bab7 100644 --- a/packages/support/src/concerns/exceptions/ConcernError.ts +++ b/packages/support/src/concerns/exceptions/ConcernError.ts @@ -10,12 +10,13 @@ export default class ConcernError extends Error implements ConcernException { /** * The Concern class that caused this error or exception - * - * @private * * @type {ConcernConstructor | null} + * + * @protected + * @readonly */ - readonly #concern: ConcernConstructor | null + protected readonly _concern: ConcernConstructor | null /** * Create a new Concern Error instance @@ -30,7 +31,7 @@ export default class ConcernError extends Error implements ConcernException configureCustomError(this); - this.#concern = concern; + this._concern = concern; // Force set the concern in the cause (in case custom was provided) (this.cause as Record).concern = concern; @@ -45,6 +46,6 @@ export default class ConcernError extends Error implements ConcernException */ get concern(): ConcernConstructor | null { - return this.#concern; + return this._concern; } } \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/InjectionError.ts b/packages/support/src/concerns/exceptions/InjectionError.ts index 3f83ffbb..92553c30 100644 --- a/packages/support/src/concerns/exceptions/InjectionError.ts +++ b/packages/support/src/concerns/exceptions/InjectionError.ts @@ -13,11 +13,12 @@ export default class InjectionError extends ConcernError implements InjectionExc /** * The target class * - * @readonly - * * @type {ConstructorLike|UsesConcerns} + * + * @protected + * @readonly */ - readonly #target: ConstructorLike | UsesConcerns; + protected readonly _target: ConstructorLike | UsesConcerns; /** * Create a new Injection Error instance @@ -37,7 +38,7 @@ export default class InjectionError extends ConcernError implements InjectionExc configureCustomError(this); - this.#target = target; + this._target = target; // Force set the target in the cause (this.cause as Record).target = target; @@ -52,6 +53,6 @@ export default class InjectionError extends ConcernError implements InjectionExc */ get target(): ConstructorLike | UsesConcerns { - return this.#target; + return this._target; } } \ No newline at end of file diff --git a/packages/support/src/concerns/exceptions/UnsafeAliasError.ts b/packages/support/src/concerns/exceptions/UnsafeAliasError.ts index 9ed520a9..bbed4648 100644 --- a/packages/support/src/concerns/exceptions/UnsafeAliasError.ts +++ b/packages/support/src/concerns/exceptions/UnsafeAliasError.ts @@ -15,20 +15,22 @@ export default class UnsafeAliasError extends InjectionError implements UnsafeAl /** * The alias that points to an "unsafe" property or method * - * @readonly - * * @type {PropertyKey} + * + * @protected + * @readonly */ - readonly #alias: PropertyKey; + protected readonly _alias: PropertyKey; /** * The "unsafe" property or method that an alias points to * - * @readonly - * * @type {PropertyKey} + * + * @protected + * @readonly */ - readonly #key: PropertyKey; + protected readonly _key: PropertyKey; /** * Create a new Unsafe Alias Error instance @@ -53,8 +55,8 @@ export default class UnsafeAliasError extends InjectionError implements UnsafeAl configureCustomError(this); - this.#alias = alias; - this.#key = key; + this._alias = alias; + this._key = key; // Force set the key and alias in the cause (this.cause as Record).alias = alias; @@ -70,7 +72,7 @@ export default class UnsafeAliasError extends InjectionError implements UnsafeAl */ get alias(): PropertyKey { - return this.#alias; + return this._alias; } /** @@ -82,6 +84,6 @@ export default class UnsafeAliasError extends InjectionError implements UnsafeAl */ get key(): PropertyKey { - return this.#key; + return this._key; } } \ No newline at end of file diff --git a/packages/support/src/meta/MetaRepository.ts b/packages/support/src/meta/MetaRepository.ts index 81104909..23b9e2d4 100644 --- a/packages/support/src/meta/MetaRepository.ts +++ b/packages/support/src/meta/MetaRepository.ts @@ -39,9 +39,10 @@ export default class MetaRepository implements Repository * * @type {object} * - * @private + * @protected + * @readonly */ - readonly #owner: object; + protected readonly _owner: object; /** * Create a new Meta Repository instance @@ -49,7 +50,7 @@ export default class MetaRepository implements Repository * @param {object} owner */ constructor(owner: object) { - this.#owner = owner; + this._owner = owner; } /** @@ -71,7 +72,7 @@ export default class MetaRepository implements Repository */ public get owner(): object { - return this.#owner; + return this._owner; } /** diff --git a/packages/support/src/mixins/Builder.ts b/packages/support/src/mixins/Builder.ts index f2a1d658..d573375d 100644 --- a/packages/support/src/mixins/Builder.ts +++ b/packages/support/src/mixins/Builder.ts @@ -18,10 +18,13 @@ export default class Builder /** * The target superclass * + * @template T = object + * * @type {ConstructorLike} - * @private + * + * @protected */ - readonly #superclass: ConstructorLike; + protected readonly _superclass: ConstructorLike; /** * Create a new Mixin Builder instance @@ -29,7 +32,7 @@ export default class Builder * @param {ConstructorLike} [superclass=class {}] */ constructor(superclass:ConstructorLike = class {} as ConstructorLike) { - this.#superclass = superclass; + this._superclass = superclass; } /** @@ -52,7 +55,7 @@ export default class Builder // Apply the mixin... return mixin(superclass); - }, this.#superclass as ConstructorLike) as ConstructorLike; + }, this._superclass as ConstructorLike) as ConstructorLike; } /** @@ -69,6 +72,6 @@ export default class Builder ...mixins: MixinFunction[] /* eslint-disable-line @typescript-eslint/no-unused-vars */ ): ConstructorLike { - return this.#superclass; + return this._superclass; } } \ No newline at end of file diff --git a/packages/support/src/objects/ObjectId.ts b/packages/support/src/objects/ObjectId.ts index 144ce79f..ef5160af 100644 --- a/packages/support/src/objects/ObjectId.ts +++ b/packages/support/src/objects/ObjectId.ts @@ -14,17 +14,21 @@ export default class ObjectId * Internal counter * * @type {number} - * @private + * + * @protected + * @static */ - static #count: number = 0; + protected static _count: number = 0; /** * Weak Map of objects and their associated id * * @type {WeakMap} - * @private + * + * @protected + * @readonly */ - static #map: WeakMap = new WeakMap(); + protected static _map: WeakMap = new WeakMap(); /** * Returns a unique ID for target object. @@ -38,15 +42,15 @@ export default class ObjectId */ static get(target: object): number { - const id: number | undefined = ObjectId.#map.get(target); + const id: number | undefined = ObjectId._map.get(target); if (id !== undefined) { return id; } - ObjectId.#count += 1; - ObjectId.#map.set(target, ObjectId.#count); + ObjectId._count += 1; + ObjectId._map.set(target, ObjectId._count); - return ObjectId.#count; + return ObjectId._count; } /** @@ -58,6 +62,6 @@ export default class ObjectId */ static has(target: object): boolean { - return ObjectId.#map.has(target); + return ObjectId._map.has(target); } } \ No newline at end of file diff --git a/packages/support/src/objects/merge/Merger.ts b/packages/support/src/objects/merge/Merger.ts index 47dd696b..fbc144b0 100644 --- a/packages/support/src/objects/merge/Merger.ts +++ b/packages/support/src/objects/merge/Merger.ts @@ -23,21 +23,22 @@ export default class Merger implements ObjectsMerger { /** * The merge options to be applied - * - * @private * * @type {Readonly} + * + * @protected */ - #options: Readonly; + protected _options: Readonly; /** * Callback to perform the merging of nested objects. * - * @private - * * @type {NextCallback} + * + * @protected + * @readonly */ - readonly #next: NextCallback; + protected readonly _next: NextCallback; /** * Create a new objects merger instance @@ -48,8 +49,8 @@ export default class Merger implements ObjectsMerger */ 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._options = null; + this._next = this.merge; this.using(options); } @@ -61,7 +62,7 @@ export default class Merger implements ObjectsMerger */ get options(): Readonly { - return this.#options; + return this._options; } /** @@ -69,7 +70,7 @@ export default class Merger implements ObjectsMerger */ get nextCallback(): NextCallback { - return this.#next; + return this._next; } /** @@ -83,7 +84,7 @@ export default class Merger implements ObjectsMerger */ public using(options?: MergeCallback | MergeOptions): this { - this.#options = this.resolveOptions(options); + this._options = this.resolveOptions(options); return this; } From 33ecd57b36cacdba5fde58138034d29911421b5c Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 13:31:56 +0200 Subject: [PATCH 185/224] Change release notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2bdd3a..eb1e2d68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Root package Typescript dependency changed to `^5.4.2`. * `@typescript-eslint/eslint-plugin` upgraded to `^7.1.1`, in root package. +* Refactored all classes' fields, changed from private to protected visibility (_see [private is not inherited](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain) for details in_). * Removed decorator return types for `use()`, `meta()`, `targetMeta()`, and `inheritTargetMeta()` (_continued to cause TS1270 and TS1238 errors_). [#8](https://github.com/aedart/ion/pull/8), [#9](https://github.com/aedart/ion/pull/9). * Refactored `hasAllMethods()` to use new `isMethod()` internally, in `@aedart/support/reflections`. * Refactored all components that used deprecated `ConstructorOrAbstractConstructor` to use new `ConstructorLike` type alias. From 54953ec14118f43e5564ed8c9f40df3a4ed76e0d Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 13:33:32 +0200 Subject: [PATCH 186/224] Change release note --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb1e2d68..6387e586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `isCallbackWrapper` util, in `@aedart/support`. * `ArbitraryData` concern, in `@aedart/support`. * `arrayMergeOptions` in object `merge()`. -* Add upgrade guide from v0.7.x- to v0.10.x. +* Add upgrade guide for "v0.7.x- to v0.10.x". ### Changed From 801d77b20c4f60dfc0ec7a1ac78f71b8eca5247a Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 14:05:25 +0200 Subject: [PATCH 187/224] Add Contextual Binding Builder interface --- .../src/container/ContextualBindingBuilder.ts | 30 +++++++++++++++++++ packages/contracts/src/container/index.ts | 4 ++- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 packages/contracts/src/container/ContextualBindingBuilder.ts diff --git a/packages/contracts/src/container/ContextualBindingBuilder.ts b/packages/contracts/src/container/ContextualBindingBuilder.ts new file mode 100644 index 00000000..aebea18e --- /dev/null +++ b/packages/contracts/src/container/ContextualBindingBuilder.ts @@ -0,0 +1,30 @@ +import { Constructor } from "@aedart/contracts"; +import { Identifier, FactoryCallback } from "./types"; + +/** + * Contextual Binding Builder + * + * Adaptation of Laravel's Contextual Binding Builder. + * + * @see https://github.com/laravel/framework/blob/master/src/Illuminate/Contracts/Container/ContextualBindingBuilder.php + */ +export default interface ContextualBindingBuilder +{ + /** + * Define the target identifier in this context. + * + * @param {Identifier} identifier + * + * @return {this} + */ + needs(identifier: Identifier): this; + + /** + * Define the implementation to be resolved in this context. + * + * @param {FactoryCallback | Constructor} implementation + * + * @return {void} + */ + give(implementation: FactoryCallback | Constructor): void; +} \ No newline at end of file diff --git a/packages/contracts/src/container/index.ts b/packages/contracts/src/container/index.ts index a27576b9..9e27f986 100644 --- a/packages/contracts/src/container/index.ts +++ b/packages/contracts/src/container/index.ts @@ -17,9 +17,11 @@ export const DEPENDENCIES: unique symbol = Symbol('dependencies'); import Binding from "./Binding"; import Container from "./Container"; +import ContextualBindingBuilder from "./ContextualBindingBuilder"; export { type Binding, - type Container + type Container, + type ContextualBindingBuilder, } export * from './exceptions/index'; From 8ea50a0426ddfefecfd7ac3d9a214444b2a26270 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 14:33:08 +0200 Subject: [PATCH 188/224] Change give(), allow TypeError to be thrown --- packages/contracts/src/container/ContextualBindingBuilder.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/contracts/src/container/ContextualBindingBuilder.ts b/packages/contracts/src/container/ContextualBindingBuilder.ts index aebea18e..14781963 100644 --- a/packages/contracts/src/container/ContextualBindingBuilder.ts +++ b/packages/contracts/src/container/ContextualBindingBuilder.ts @@ -25,6 +25,8 @@ export default interface ContextualBindingBuilder * @param {FactoryCallback | Constructor} implementation * * @return {void} + * + * @throws {TypeError} */ give(implementation: FactoryCallback | Constructor): void; } \ No newline at end of file From af894b788706591c33c6f9dde7f8063379a26380 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 14:35:37 +0200 Subject: [PATCH 189/224] Add concrete Contextual Binding Builder --- packages/container/src/Builder.ts | 86 +++++++++++++++++++++++++++++++ packages/container/src/index.ts | 2 + 2 files changed, 88 insertions(+) create mode 100644 packages/container/src/Builder.ts diff --git a/packages/container/src/Builder.ts b/packages/container/src/Builder.ts new file mode 100644 index 00000000..67b1281b --- /dev/null +++ b/packages/container/src/Builder.ts @@ -0,0 +1,86 @@ +import type { + Container, + ContextualBindingBuilder, + FactoryCallback, + Identifier +} from "@aedart/contracts/container"; +import type { Constructor } from "@aedart/contracts"; +import { castArray } from 'lodash-es'; + +/** + * Contextual Binding Builder + * + * Adaptation of Laravel Contextual Binding Builder + * + * @see https://github.com/laravel/framework/blob/master/src/Illuminate/Container/ContextualBindingBuilder.php + */ +export default class Builder implements ContextualBindingBuilder +{ + /** + * The service container to be used in this context. + * + * @type {Container} + * + * @protected + */ + protected container: Container; + + /** + * The concrete constructor(s) + * + * @type {Constructor | Constructor[]} + * + * @protected + */ + protected concrete: Constructor | Constructor[]; + + /** + * The target identifier in this context. + * + * @type {Identifier} + * + * @protected + */ + protected identifier: Identifier | undefined = undefined; + + /** + * Create a new Contextual Binding Builder instance + * + * @param {Container} container + * @param {Constructor | Constructor[]} concrete + */ + constructor(container: Container, concrete: Constructor | Constructor[]) { + this.container = container; + this.concrete = concrete; + } + + /** + * Define the target identifier in this context. + * + * @param {Identifier} identifier + * + * @return {this} + */ + public needs(identifier: Identifier): this + { + this.identifier = identifier; + + return this; + } + + /** + * Define the implementation to be resolved in this context. + * + * @param {FactoryCallback | Constructor} implementation + * + * @return {void} + */ + public give(implementation: FactoryCallback | Constructor): void + { + const constructors: Constructor[] = castArray(this.concrete); + + for(const concrete of constructors) { + this.container.addContextualBinding(concrete, this.identifier as Identifier, implementation); + } + } +} \ No newline at end of file diff --git a/packages/container/src/index.ts b/packages/container/src/index.ts index 906cf316..cc16885b 100644 --- a/packages/container/src/index.ts +++ b/packages/container/src/index.ts @@ -1,6 +1,8 @@ +import Builder from "./Builder"; import BindingEntry from "./BindingEntry"; import Container from "./Container"; export { + Builder, BindingEntry, Container } From a9a18b3094ab45e776dcfe68168996569dc30c61 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 15:38:55 +0200 Subject: [PATCH 190/224] Mark lodash-es as external dependency Uh - might require a more intelligent rollup config helper, such that it can automatically detect peer dependencies from required packages. --- packages/container/rollup.config.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/container/rollup.config.mjs b/packages/container/rollup.config.mjs index 45ff44b6..d7737616 100644 --- a/packages/container/rollup.config.mjs +++ b/packages/container/rollup.config.mjs @@ -22,5 +22,7 @@ export default createConfig({ '@aedart/support/misc', '@aedart/support/mixins', '@aedart/support/reflections', + + 'lodash-es' ] }); From 80fe7d2c0754092ee57c7bee196fc9983bf3be06 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 15:40:13 +0200 Subject: [PATCH 191/224] Change Container, add support for contextual binding --- packages/container/src/Container.ts | 92 +++++++++++++++++++ packages/contracts/src/container/Container.ts | 29 ++++++ 2 files changed, 121 insertions(+) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 0fd7e8b6..53d93a0e 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -3,6 +3,7 @@ import { Alias, BeforeResolvedCallback, Container as ServiceContainerContract, + ContextualBindingBuilder, ExtendCallback, FactoryCallback, Identifier, @@ -33,6 +34,7 @@ import { import CircularDependencyError from "./exceptions/CircularDependencyError"; import ContainerError from "./exceptions/ContainerError"; import NotFoundError from "./exceptions/NotFoundError"; +import Builder from "./Builder"; import BindingEntry from "./BindingEntry"; import { isBinding } from "./isBinding"; @@ -100,6 +102,18 @@ export default class Container implements ServiceContainerContract * @protected */ protected resolved: Set = new Set(); + + /** + * Contextual Bindings + * + * @type {Map>} + * + * @protected + */ + protected contextualBindings: Map< + Constructor, + Map + > = new Map(); /** * "Before" resolved callbacks @@ -269,6 +283,75 @@ export default class Container implements ServiceContainerContract return instance; } + /** + * Add a contextual binding in this container + * + * @param {Constructor} concrete + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} implementation + * + * @return {this} + * + * @throws {TypeError} + */ + + public addContextualBinding( + concrete: Constructor, + identifier: Identifier, + implementation: FactoryCallback | Constructor + ): this + { + if (!this.contextualBindings.has(concrete)) { + this.contextualBindings.set(concrete, new Map()); + } + + const entry = this.contextualBindings.get(concrete) as Map; + entry.set(this.getAlias(identifier), this.makeBindingEntry(identifier, implementation)); + + return this; + } + + /** + * Define a contextual binding + * + * @param {Constructor | Constructor[]} concrete + * + * @return {ContextualBindingBuilder} + * + * @throws {TypeError} + */ + public when(concrete: Constructor | Constructor[]): ContextualBindingBuilder + { + return new Builder(this, concrete); + } + + /** + * Determine if a contextual binding is registered for target + * + * @param {Constructor} target + * @param {Identifier} identifier + * + * @return {boolean} + */ + public hasContextualBinding(target: Constructor, identifier: Identifier): boolean + { + return this.contextualBindings.has(target) + && (this.contextualBindings.get(target) as Map).has(identifier); + } + + /** + * Returns contextual binding implementation for given target and identifier + * + * @param {Constructor} target + * @param {Identifier} identifier + * + * @return {Binding | undefined} + */ + public getContextualBinding(target: Constructor, identifier: Identifier): Binding | undefined + { + return this.contextualBindings.get(target)?.get(identifier); + } + /** * Resolves binding value that matches identifier and returns it * @@ -993,6 +1076,15 @@ export default class Container implements ServiceContainerContract protected resolveDependency(identifier: Identifier, target: object): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ { try { + // In case that the target is a constructor that has a contextual binding defined + // for given identifier, then it must be resolved. + if (this.hasContextualBinding(target as Constructor, identifier)) { + const binding = this.getContextualBinding(target as Constructor, identifier); + + return this.build(binding as Binding); + } + + // Otherwise, just resolve the given binding identifier... return this.make(identifier); } catch (e) { if (e instanceof CircularDependencyError) { diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index b75ff480..1f7197ba 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -14,6 +14,7 @@ import { AfterResolvedCallback, } from "./types"; import Binding from "./Binding"; +import ContextualBindingBuilder from "./ContextualBindingBuilder"; /** * Service Container @@ -88,6 +89,34 @@ export default interface Container * @throws {TypeError} */ instance(identifier: Identifier, instance: T): T; + + /** + * Add a contextual binding in this container + * + * @param {Constructor} concrete + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} implementation + * + * @return {this} + * + * @throws {TypeError} + */ + addContextualBinding( + concrete: Constructor, + identifier: Identifier, + implementation: FactoryCallback | Constructor + ): this; + + /** + * Define a contextual binding + * + * @param {Constructor | Constructor[]} concrete + * + * @return {ContextualBindingBuilder} + * + * @throws {TypeError} + */ + when(concrete: Constructor | Constructor[]): ContextualBindingBuilder; /** * Resolves binding value that matches identifier and returns it From 87f4ace92fe9cc3eca776fc67611c4e31e1cbb3a Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 15:52:54 +0200 Subject: [PATCH 192/224] Add hasContextualBindings() Also changed the hasContextualBinding() to use new method internally. --- packages/container/src/Container.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 53d93a0e..bdc6a480 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -326,7 +326,20 @@ export default class Container implements ServiceContainerContract } /** - * Determine if a contextual binding is registered for target + * Determine if target has one or more contextual bindings registered + * + * @param {Constructor} target + * + * @return {boolean} + */ + public hasContextualBindings(target: Constructor): boolean + { + return this.contextualBindings.has(target) && + (this.contextualBindings.get(target) as Map).size > 0; + } + + /** + * Determine if a contextual binding is registered for the identifier in given target * * @param {Constructor} target * @param {Identifier} identifier @@ -335,7 +348,7 @@ export default class Container implements ServiceContainerContract */ public hasContextualBinding(target: Constructor, identifier: Identifier): boolean { - return this.contextualBindings.has(target) + return this.hasContextualBindings(target) && (this.contextualBindings.get(target) as Map).has(identifier); } From 553c9a77ea18f329bb9121a0c7a35f9b1599e72b Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 16:12:11 +0200 Subject: [PATCH 193/224] Refactor, use spread arg. for concrete This allows us to remove the need for using lodash's castArray() --- packages/container/src/Builder.ts | 15 ++++++--------- packages/container/src/Container.ts | 6 +++--- packages/contracts/src/container/Container.ts | 4 ++-- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/container/src/Builder.ts b/packages/container/src/Builder.ts index 67b1281b..e61e4c6a 100644 --- a/packages/container/src/Builder.ts +++ b/packages/container/src/Builder.ts @@ -5,7 +5,6 @@ import type { Identifier } from "@aedart/contracts/container"; import type { Constructor } from "@aedart/contracts"; -import { castArray } from 'lodash-es'; /** * Contextual Binding Builder @@ -28,11 +27,11 @@ export default class Builder implements ContextualBindingBuilder /** * The concrete constructor(s) * - * @type {Constructor | Constructor[]} + * @type {Constructor[]} * * @protected */ - protected concrete: Constructor | Constructor[]; + protected concrete: Constructor[]; /** * The target identifier in this context. @@ -47,9 +46,9 @@ export default class Builder implements ContextualBindingBuilder * Create a new Contextual Binding Builder instance * * @param {Container} container - * @param {Constructor | Constructor[]} concrete + * @param {...Constructor[]} concrete */ - constructor(container: Container, concrete: Constructor | Constructor[]) { + constructor(container: Container, ...concrete: Constructor[]) { this.container = container; this.concrete = concrete; } @@ -77,10 +76,8 @@ export default class Builder implements ContextualBindingBuilder */ public give(implementation: FactoryCallback | Constructor): void { - const constructors: Constructor[] = castArray(this.concrete); - - for(const concrete of constructors) { - this.container.addContextualBinding(concrete, this.identifier as Identifier, implementation); + for(const ctor of this.concrete) { + this.container.addContextualBinding(ctor, this.identifier as Identifier, implementation); } } } \ No newline at end of file diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index bdc6a480..595cfd30 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -314,15 +314,15 @@ export default class Container implements ServiceContainerContract /** * Define a contextual binding * - * @param {Constructor | Constructor[]} concrete + * @param {...Constructor[]} concrete * * @return {ContextualBindingBuilder} * * @throws {TypeError} */ - public when(concrete: Constructor | Constructor[]): ContextualBindingBuilder + public when(...concrete: Constructor[]): ContextualBindingBuilder { - return new Builder(this, concrete); + return new Builder(this, ...concrete); } /** diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts index 1f7197ba..5c9625ea 100644 --- a/packages/contracts/src/container/Container.ts +++ b/packages/contracts/src/container/Container.ts @@ -110,13 +110,13 @@ export default interface Container /** * Define a contextual binding * - * @param {Constructor | Constructor[]} concrete + * @param {...Constructor[]} concrete * * @return {ContextualBindingBuilder} * * @throws {TypeError} */ - when(concrete: Constructor | Constructor[]): ContextualBindingBuilder; + when(...concrete: Constructor[]): ContextualBindingBuilder; /** * Resolves binding value that matches identifier and returns it From 2584da62ecbafbe84c4dc54c69e6d65a865cb971 Mon Sep 17 00:00:00 2001 From: alin Date: Thu, 4 Apr 2024 16:16:25 +0200 Subject: [PATCH 194/224] Add tests for contextual bindings --- .../container/contextual-bindings.test.js | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 tests/browser/packages/container/container/contextual-bindings.test.js diff --git a/tests/browser/packages/container/container/contextual-bindings.test.js b/tests/browser/packages/container/container/contextual-bindings.test.js new file mode 100644 index 00000000..49b0bed3 --- /dev/null +++ b/tests/browser/packages/container/container/contextual-bindings.test.js @@ -0,0 +1,98 @@ +import { Container } from "@aedart/container"; +import { dependencies } from "@aedart/support/container"; + +describe('@aedart/support/container', () => { + describe('addContextualBinding', () => { + + it('register and resolve contextual binding', () => { + const container = new Container(); + + class A {} + + class B {} + + @dependencies(A) + class C { + resolved; + constructor(resolved) { + this.resolved = resolved; + } + } + + // ----------------------------------------------------------------- // + + container + .when(C) + .needs(A) + .give(B); + + // ----------------------------------------------------------------- // + + expect(container.hasContextualBindings(C)) + .withContext('Container SHOULD have contextual bindings registered for C') + .toBeTrue(); + + expect(container.hasContextualBinding(C, A)) + .withContext('Container SHOULD have a contextual binding registered') + .toBeTrue(); + + const result = container.make(C); + expect(result) + .withContext('Incorrect instance resolved (C expected)') + .toBeInstanceOf(C); + + expect(result.resolved) + .withContext('Incorrect dependency resolved (B expected) for C') + .toBeInstanceOf(B); + }); + + it('can register contextual binding for multiple constructors', () => { + const container = new Container(); + + const identifier = 'storage'; + + @dependencies(identifier) + class ServiceA { + storage; + + constructor(storage) { + this.storage = storage; + } + } + + @dependencies(identifier) + class ServiceB { + storage; + + constructor(storage) { + this.storage = storage; + } + } + + class MyStorage {} + + // ----------------------------------------------------------------- // + + const storage = new MyStorage(); + container + .when(ServiceA, ServiceB) + .needs(identifier) + .give(() => { + return storage; + }); + + // ----------------------------------------------------------------- // + + const a = container.make(ServiceA); + const b = container.make(ServiceB); + + expect(a.storage) + .withContext('Incorrect resolved for A') + .toBe(storage); + + expect(b.storage) + .withContext('Incorrect resolved for B') + .toBe(storage); + }); + }); +}); \ No newline at end of file From 63745f3ec25d7ae2ba100cd4ce2157e98dc39d02 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 10:50:31 +0200 Subject: [PATCH 195/224] Clear contextual bindings when flush() is invoked --- packages/container/src/Container.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 595cfd30..6a9780c2 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -700,6 +700,7 @@ export default class Container implements ServiceContainerContract this.instances.clear(); this.aliases.clear(); this.resolved.clear(); + this.contextualBindings.clear(); this.resolveStack.clear(); } From e8d368aa2d68a8b684990b9d07c54717e328056e Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 10:52:52 +0200 Subject: [PATCH 196/224] Rename _instance to "instance" --- packages/container/src/Container.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/container/src/Container.ts b/packages/container/src/Container.ts index 6a9780c2..f8ce4b07 100644 --- a/packages/container/src/Container.ts +++ b/packages/container/src/Container.ts @@ -56,7 +56,7 @@ export default class Container implements ServiceContainerContract * * @static */ - protected static _instance: ServiceContainerContract | null = null; + protected static instance: ServiceContainerContract | null = null; /** * Registered bindings @@ -158,11 +158,11 @@ export default class Container implements ServiceContainerContract */ public static getInstance(): ServiceContainerContract { - if (this._instance === null) { + if (this.instance === null) { this.setInstance(new this()); } - return this._instance as ServiceContainerContract; + return this.instance as ServiceContainerContract; } /** @@ -174,7 +174,7 @@ export default class Container implements ServiceContainerContract */ public static setInstance(container: ServiceContainerContract | null = null): ServiceContainerContract | null { - return this._instance = container; + return this.instance = container; } /** From e1e4235cab5c48324ed682dbe0cce6fb5034f8f2 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 13:02:14 +0200 Subject: [PATCH 197/224] Add description of service container package --- packages/container/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/container/README.md b/packages/container/README.md index 3fe7f648..2e4982a9 100644 --- a/packages/container/README.md +++ b/packages/container/README.md @@ -1,6 +1,12 @@ # Service Container -TODO: ... +The `@aedart/container` package offers an adaptation of [Laravel's Service Container](https://laravel.com/docs/11.x/container) package +(_originally licensed under [MIT](https://github.com/laravel/framework/blob/11.x/src/Illuminate/Container/LICENSE.md)_). + +The tools provided by this package give you way to: + +* Manage class dependencies +* Perform [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) # Official Documentation From 7594ba350351303c9421cae37e4ca979e3cc4f51 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 13:58:01 +0200 Subject: [PATCH 198/224] Add Service Container introduction --- .../current/packages/container/README.md | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/archive/current/packages/container/README.md diff --git a/docs/archive/current/packages/container/README.md b/docs/archive/current/packages/container/README.md new file mode 100644 index 00000000..f2667841 --- /dev/null +++ b/docs/archive/current/packages/container/README.md @@ -0,0 +1,83 @@ +--- +title: Introduction +description: Ion Service Container package +sidebarDepth: 0 +--- + +# Introduction + +The `@aedart/container` package offers an adaptation of [Laravel's Service Container](https://laravel.com/docs/11.x/container) package +(_originally licensed under [MIT](https://github.com/laravel/framework/blob/11.x/src/Illuminate/Container/LICENSE.md)_). + +The tools provided by this package give you way to: +* Manage class dependencies +* Perform [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) + +## Example + +### Bindings + +Imagine that you have an Api client (_or any component for that matter_). Whenever it is needed, you want it to be +injected into components that depend on it. + +```js +export default class ApiClient +{ + // ...implementation not shown... +} +``` + +To ensure that dependency injection can be performed, you must first bind the component in the service container. +Each binding requires a unique identifier, e.g. a string, symbol, number...etc. + +```js +import { Container } from "@aedart/container"; +import { ApiClient } from "@acme/api"; + +const container = Container.getInstance(); + +// Bind 'my_api_client' to the ApiClient component... +container.bind('my_api_client', ApiClient); +``` + +### Define Dependencies + +To define the dependencies of a component, use the `dependencies()` decorator. +By itself, the decorator does not do anything more than to associate a component with one or more dependencies +(_binding identifiers_). In other words, the decorator _**does not automatically inject**_ anything into your class. +It only registers the dependencies as [metadata](../support/meta) onto a class. + +```js +import { dependencies } from "@aedart/support/container"; + +@dependencies('my_api_client') +export default class BookService +{ + apiClient; + + constructor(client) { + this.apiClient = client; + } + + // ...remaining not shown... +} +``` + +### Resolve + +When you want to resolve a component, with all of its dependencies injected into it, use the service container's `make()` +method. + +```js +import { Container } from "@aedart/container"; +import { BookService } from "@acme/app/services"; + +const bookService = Container.getInstance().make(BookService); + +console.log(bookService.apiClient); // ApiClient +``` + +### Onward + +The above shown example illustrates the most basic usage of the service container. Throughout the remaining of this +package's documentation, more examples and use-cases are covered. \ No newline at end of file From 9a53949a9f78a2c1f5afebbeccb3e670c3dc9f19 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 13:58:26 +0200 Subject: [PATCH 199/224] Add prerequisites for using service container --- .../current/packages/container/prerequisites.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 docs/archive/current/packages/container/prerequisites.md diff --git a/docs/archive/current/packages/container/prerequisites.md b/docs/archive/current/packages/container/prerequisites.md new file mode 100644 index 00000000..b1bf596a --- /dev/null +++ b/docs/archive/current/packages/container/prerequisites.md @@ -0,0 +1,10 @@ +--- +title: Prerequisites +description: Prerequisites for using service container. +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 service container, 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 From 463927a75faeda83cf67b7ec8bb1d4476d3ebf9c Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 13:58:36 +0200 Subject: [PATCH 200/224] Add how to install guide for service container --- .../current/packages/container/install.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/archive/current/packages/container/install.md diff --git a/docs/archive/current/packages/container/install.md b/docs/archive/current/packages/container/install.md new file mode 100644 index 00000000..a54a770e --- /dev/null +++ b/docs/archive/current/packages/container/install.md @@ -0,0 +1,24 @@ +--- +description: How to install Ion Service Container package +sidebarDepth: 0 +--- + +# How to install + +## npm + +```bash:no-line-numbers +npm install --save-peer @aedart/container +``` + +## yarn + +```bash:no-line-numbers +yarn add --peer @aedart/container +``` + +## pnpm + +```bash:no-line-numbers +pnpm add --save-peer @aedart/container +``` \ No newline at end of file From 768f2b33e75de41ecc2eaa77b3ab72df6afa54f5 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 13:58:50 +0200 Subject: [PATCH 201/224] Add guide for how to obtain container instance --- .../packages/container/container-instance.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/archive/current/packages/container/container-instance.md diff --git a/docs/archive/current/packages/container/container-instance.md b/docs/archive/current/packages/container/container-instance.md new file mode 100644 index 00000000..9d7aeec1 --- /dev/null +++ b/docs/archive/current/packages/container/container-instance.md @@ -0,0 +1,34 @@ +--- +description: How to obtain Service Container instance +sidebarDepth: 0 +--- + +# Container Instance + +The Service Container can be instantiated as a new instance, if you wish it. This allows you to use the container in +isolation, without application-wide side effects. + +```js +import { Container } from "@aedart/container"; + +const container = new Container(); +``` + +However, if you plan the use the same Service Container instance across your entire application, then you can obtain a [singleton](https://en.wikipedia.org/wiki/Singleton_pattern) +instance via the static method `getInstance()`. + +The `getInstance()` method will automatically create a new Service Container instance and store a static reference to it, if no previous instance +was created. + +```js +const container = Container.getInstance(); +``` + +## Destroy Existing Instance + +In situations when you need to destroy the existing singleton instance, call the static `setInstance()` method +with `null` as argument. + +```js +Container.setInstance(null); // Existing singleton instance is now lost... +``` \ No newline at end of file From 3d2f7ad6f015c3eee5f74f7e7bc1df5795c324e8 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 13:59:02 +0200 Subject: [PATCH 202/224] Add bindings section (incomplete) --- .../current/packages/container/bindings.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/archive/current/packages/container/bindings.md diff --git a/docs/archive/current/packages/container/bindings.md b/docs/archive/current/packages/container/bindings.md new file mode 100644 index 00000000..2b39f872 --- /dev/null +++ b/docs/archive/current/packages/container/bindings.md @@ -0,0 +1,50 @@ +--- +description: Service Container Bindings +sidebarDepth: 0 +--- + +# Bindings + +[[TOC]] + +## Basics + +### Regular + +TODO... + +### Singletons + +TODO... + +### Instances + +TODO... + +## Identifiers + +To resolve the correct component instance or value from the Service Container, binding identifiers must be unique. +The following types are supported as binding identifiers: + +* `string` +* `symbol` +* `number` +* `object` (_not `null`_) +* Class Constructor +* Callback + +## Constructors + +TODO... + +## Factory Callbacks + +TODO... + +## Contextual Bindings + +TODO... + +## Extend Bindings + +TODO... \ No newline at end of file From da04c93389b94fcf022275588d08e90a2115d171 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 13:59:23 +0200 Subject: [PATCH 203/224] Add how to resolve dependencies guide (incomplete) --- docs/archive/current/packages/container/resolving.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/archive/current/packages/container/resolving.md diff --git a/docs/archive/current/packages/container/resolving.md b/docs/archive/current/packages/container/resolving.md new file mode 100644 index 00000000..bf93f3dc --- /dev/null +++ b/docs/archive/current/packages/container/resolving.md @@ -0,0 +1,8 @@ +--- +description: Resolving Dependencies +sidebarDepth: 0 +--- + +# Resolving + +// TODO: ... \ No newline at end of file From d03848680ec2c625e6b751f66114b03c05d2dfe2 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 13:59:48 +0200 Subject: [PATCH 204/224] Add Service Container package docs collection --- docs/.vuepress/archive/Version0x.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index df4e4ecc..037e1582 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -22,6 +22,18 @@ export default PagesCollection.make('v0.x', '/v0x', [ collapsible: true, children: [ 'packages/', + { + text: 'Container', + collapsible: true, + children: [ + 'packages/container/', + 'packages/container/prerequisites', + 'packages/container/install', + 'packages/container/container-instance', + 'packages/container/bindings', + 'packages/container/resolving', + ] + }, { text: 'Contracts', collapsible: true, From 0336af9569de9fd53ea1473fa51580a8effb0551 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 14:02:31 +0200 Subject: [PATCH 205/224] Add guide on how to define dependencies (incomplete) --- docs/archive/current/packages/container/dependencies.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/archive/current/packages/container/dependencies.md diff --git a/docs/archive/current/packages/container/dependencies.md b/docs/archive/current/packages/container/dependencies.md new file mode 100644 index 00000000..320cde49 --- /dev/null +++ b/docs/archive/current/packages/container/dependencies.md @@ -0,0 +1,8 @@ +--- +description: How to define dependencies +sidebarDepth: 0 +--- + +# Dependencies + +// TODO: ... \ No newline at end of file From f9b76a1e8febae885dcdf05bfe2e20b52a322c4b Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 14:02:40 +0200 Subject: [PATCH 206/224] Add dependencies section --- docs/.vuepress/archive/Version0x.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index 037e1582..196b85fb 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -31,6 +31,7 @@ export default PagesCollection.make('v0.x', '/v0x', [ 'packages/container/install', 'packages/container/container-instance', 'packages/container/bindings', + 'packages/container/dependencies', 'packages/container/resolving', ] }, From 81f62a5047d5e74e7e3eed5ece67060241040888 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 15:13:34 +0200 Subject: [PATCH 207/224] Add guide for how to define contextual bindings (incomplete) --- .../current/packages/container/contextual-bindings.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/archive/current/packages/container/contextual-bindings.md diff --git a/docs/archive/current/packages/container/contextual-bindings.md b/docs/archive/current/packages/container/contextual-bindings.md new file mode 100644 index 00000000..01a277db --- /dev/null +++ b/docs/archive/current/packages/container/contextual-bindings.md @@ -0,0 +1,8 @@ +--- +description: Define Contextual Bindings +sidebarDepth: 0 +--- + +# Contextual Bindings + +// TODO: ... \ No newline at end of file From acf309c0a21897b8b40ea0518cd5cd1d26a8a89e Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 15:13:40 +0200 Subject: [PATCH 208/224] Add contextual bindings --- docs/.vuepress/archive/Version0x.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index 196b85fb..27ba6975 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -33,6 +33,7 @@ export default PagesCollection.make('v0.x', '/v0x', [ 'packages/container/bindings', 'packages/container/dependencies', 'packages/container/resolving', + 'packages/container/contextual-bindings', ] }, { From 5929676583c510b50ae7fd19ef432c568d9fbb36 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 15:18:08 +0200 Subject: [PATCH 209/224] Add make() method section (incomplete) --- docs/archive/current/packages/container/resolving.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/archive/current/packages/container/resolving.md b/docs/archive/current/packages/container/resolving.md index bf93f3dc..d9c01275 100644 --- a/docs/archive/current/packages/container/resolving.md +++ b/docs/archive/current/packages/container/resolving.md @@ -5,4 +5,8 @@ sidebarDepth: 0 # Resolving +// TODO: ... + +## The `make()` method + // TODO: ... \ No newline at end of file From 9bd052dc646d0d0b11b6a0d3df803b94f1d716f5 Mon Sep 17 00:00:00 2001 From: alin Date: Fri, 5 Apr 2024 15:33:41 +0200 Subject: [PATCH 210/224] Add binding basics, identifiers, concrete types,...etc Also, extracted the contextual bindings section into own doc. --- .../current/packages/container/bindings.md | 159 ++++++++++++++++-- 1 file changed, 146 insertions(+), 13 deletions(-) diff --git a/docs/archive/current/packages/container/bindings.md b/docs/archive/current/packages/container/bindings.md index 2b39f872..2ed4ac90 100644 --- a/docs/archive/current/packages/container/bindings.md +++ b/docs/archive/current/packages/container/bindings.md @@ -9,22 +9,64 @@ sidebarDepth: 0 ## Basics -### Regular +The `bind()` method is used to register bindings in the Service Container. It accepts three arguments: -TODO... +* `identifier: Identifier` - (_see [Identifiers](#identifiers)_). +* `concrete: FactoryCallback | Constructor` - The value to be resolved. +* `shared: boolean = false` - (_optional - see [Singletons](#singletons)_). + +```js +import { CookieStorage } from "@acme/storage"; + +container.bind('storage', CookieStorage); +``` + +When the binding is [resolved](./resolving.md) from the Service Container, the `concrete` value is returned. Either as a new class +instance (_see [Constructors](#constructors)_), or the value returned from a callback (_see [Factory Callbacks](#factory-callbacks)_). + +You can also use `bindIf()` to register a binding. The method will _ONLY_ register the binding, if one has not already +been registered for the given identifier. + +```js +container.bindIf('storage', CookieStorage); +``` ### Singletons -TODO... +If you wish to register a "shared" binding, use the `singleton()` method. It ensures that the binding is only resolved +once. This means that the same object instance or value is returned, each time that it is requested resolved. +Invoking the `singleton()` is the equivalent to invoking `bind()` with the `shared` argument set to `true`. + +```js +import { ApiClient } from "@acme/api"; + +container.singleton('api_client', ApiClient); +``` + +The `singletonIf()` method is similar to `bindIf()`. It will only register a "shared" binding, if one has not already +registered. + +```js +container.singletonIf('api_client', ApiClient); +``` ### Instances -TODO... +You can also register existing object instances in the Service Container. This is done via the `instance()` method. +Whenever the binding is requested resolved, the same instance is returned each time. + +```js +import { ApiClient } from "@acme/api"; + +const client = new ApiClient(); + +container.instance('api_client', client); +``` ## Identifiers -To resolve the correct component instance or value from the Service Container, binding identifiers must be unique. -The following types are supported as binding identifiers: +To ensure that the Service Container is able to resolve the correct object instances or values, the binding identifiers +must be unique. The following types are supported as binding identifiers: * `string` * `symbol` @@ -33,18 +75,109 @@ The following types are supported as binding identifiers: * Class Constructor * Callback -## Constructors +::: tip + +To ensure that identifiers are truly unique, you _should_ use [symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) or +[class constructors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) as binding identifiers. + +```js +// Somewhere in your application +export const STORAGE = Symbol('app_storage'); +``` + +```js +import { CookieStorage } from "@acme/storage"; +import { STORAGE } from "@acme/services"; + +container.bind(STORAGE, CookieStorage); +``` + +::: + +## `concrete` Types -TODO... +The `concrete` argument for the `bind()`, `bindIf()`, `singleton()` and `singletonIf()` methods accepts +either of the following kind of value: -## Factory Callbacks +* Class constructor +* "Factory" callback -TODO... +### Constructors -## Contextual Bindings +When registering a binding using a class constructor as the `concrete` argument value, a new instance of that class is +instantiated and returned, when requested [resolved](./resolving.md). -TODO... +```js +class TextRecorder {} + +container.bind('recorder', TextRecorder); + +// Later in your application +const recorder = container.make('recorder'); + +console.log(recorder instanceof TextRecorder); // true +``` + +### Factory Callbacks + +If you need more advanced resolve logic, then you can specify a callback as the `concrete` argument value. +When requested resolved, the callback is invoked and the Service Container instance is passed as argument to the callback. +This allows you to perform other kinds of resolve logic. + +```js +class TextRecorder { + constructor(config) { + this.config = config; + } +} + +container.bind('recorder', (container) => { + const config = container.make('my_recorder_config'); + + return new TextRecorder(config); +}); +``` + +Although the above example shows an object instance being returned by the factory callback, any kind of value can be returned. + +```js +container.bind('my_message', () => 'Hi there...'); + +// Later in your application +const msg = container.make('my_message'); // Hi there... +``` + +#### Arguments + +The factory callback is also provided with any arguments that are passed on to the [`make()` method](./resolving.md#the-make-method). + +```js +class User { + constructor(name) { + this.name = name; + } +} + +container.bind('user', (container, ...args) => { + return new User(...args); +}); + +// Later in your application +const user = container.make('user', 'Maya'); + +console.log(user.name); // Maya +``` ## Extend Bindings -TODO... \ No newline at end of file +The `extend()` method can be used to decorate or configure object instances that have been resolved. +The method accepts the following arguments: + +* `identifier: Identifier` - the target binding identifier. +* `callback: ExtendCallback` - callback that is responsible for modifying the resolved instance. + +```js +container.extend('user', (resolved, container) => { + return DecoratedUser(resolved); +}); +``` \ No newline at end of file From 84b11fc8b682a2cd597235c46c2aaddd591d67de Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 8 Apr 2024 10:39:10 +0200 Subject: [PATCH 211/224] Improve sentence --- docs/archive/current/packages/container/README.md | 2 +- packages/container/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/archive/current/packages/container/README.md b/docs/archive/current/packages/container/README.md index f2667841..6282437e 100644 --- a/docs/archive/current/packages/container/README.md +++ b/docs/archive/current/packages/container/README.md @@ -6,7 +6,7 @@ sidebarDepth: 0 # Introduction -The `@aedart/container` package offers an adaptation of [Laravel's Service Container](https://laravel.com/docs/11.x/container) package +The `@aedart/container` package offers an adaptation of [Laravel's Service Container](https://laravel.com/docs/11.x/container) (_originally licensed under [MIT](https://github.com/laravel/framework/blob/11.x/src/Illuminate/Container/LICENSE.md)_). The tools provided by this package give you way to: diff --git a/packages/container/README.md b/packages/container/README.md index 2e4982a9..45f90ca2 100644 --- a/packages/container/README.md +++ b/packages/container/README.md @@ -1,6 +1,6 @@ # Service Container -The `@aedart/container` package offers an adaptation of [Laravel's Service Container](https://laravel.com/docs/11.x/container) package +The `@aedart/container` package offers an adaptation of [Laravel's Service Container](https://laravel.com/docs/11.x/container) (_originally licensed under [MIT](https://github.com/laravel/framework/blob/11.x/src/Illuminate/Container/LICENSE.md)_). The tools provided by this package give you way to: From ffca9d8ed818031556a36a1e680b65cd091e6148 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 8 Apr 2024 10:39:53 +0200 Subject: [PATCH 212/224] Improve sentence --- docs/archive/current/packages/container/README.md | 2 +- packages/container/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/archive/current/packages/container/README.md b/docs/archive/current/packages/container/README.md index 6282437e..ee026d19 100644 --- a/docs/archive/current/packages/container/README.md +++ b/docs/archive/current/packages/container/README.md @@ -9,7 +9,7 @@ sidebarDepth: 0 The `@aedart/container` package offers an adaptation of [Laravel's Service Container](https://laravel.com/docs/11.x/container) (_originally licensed under [MIT](https://github.com/laravel/framework/blob/11.x/src/Illuminate/Container/LICENSE.md)_). -The tools provided by this package give you way to: +The tools provided by this package give you a way to: * Manage class dependencies * Perform [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) diff --git a/packages/container/README.md b/packages/container/README.md index 45f90ca2..00c771f4 100644 --- a/packages/container/README.md +++ b/packages/container/README.md @@ -3,7 +3,7 @@ The `@aedart/container` package offers an adaptation of [Laravel's Service Container](https://laravel.com/docs/11.x/container) (_originally licensed under [MIT](https://github.com/laravel/framework/blob/11.x/src/Illuminate/Container/LICENSE.md)_). -The tools provided by this package give you way to: +The tools provided by this package give you a way to: * Manage class dependencies * Perform [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) From 5fef3a3837967c23fd4a3229e9bde57c4cf03eff Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 8 Apr 2024 10:44:18 +0200 Subject: [PATCH 213/224] Improve sentences --- .../current/packages/container/container-instance.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/archive/current/packages/container/container-instance.md b/docs/archive/current/packages/container/container-instance.md index 9d7aeec1..53df8252 100644 --- a/docs/archive/current/packages/container/container-instance.md +++ b/docs/archive/current/packages/container/container-instance.md @@ -5,7 +5,7 @@ sidebarDepth: 0 # Container Instance -The Service Container can be instantiated as a new instance, if you wish it. This allows you to use the container in +The Service Container can be instantiated like any other regular class. This allows you to use the container in isolation, without application-wide side effects. ```js @@ -14,11 +14,11 @@ import { Container } from "@aedart/container"; const container = new Container(); ``` -However, if you plan the use the same Service Container instance across your entire application, then you can obtain a [singleton](https://en.wikipedia.org/wiki/Singleton_pattern) -instance via the static method `getInstance()`. +However, if you want the use the same Service Container instance across your entire application, then you can obtain a [singleton](https://en.wikipedia.org/wiki/Singleton_pattern) +instance, via the static method `getInstance()`. The `getInstance()` method will automatically create a new Service Container instance and store a static reference to it, if no previous instance -was created. +was created. Otherwise, the method will return the existing instance. ```js const container = Container.getInstance(); From eab440303f75804b9569fe75906d154293793847 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 8 Apr 2024 10:50:39 +0200 Subject: [PATCH 214/224] Improve sentences --- docs/archive/current/packages/container/bindings.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/archive/current/packages/container/bindings.md b/docs/archive/current/packages/container/bindings.md index 2ed4ac90..665f2819 100644 --- a/docs/archive/current/packages/container/bindings.md +++ b/docs/archive/current/packages/container/bindings.md @@ -12,7 +12,7 @@ sidebarDepth: 0 The `bind()` method is used to register bindings in the Service Container. It accepts three arguments: * `identifier: Identifier` - (_see [Identifiers](#identifiers)_). -* `concrete: FactoryCallback | Constructor` - The value to be resolved. +* `concrete: FactoryCallback | Constructor` - The value to be resolved from the container. * `shared: boolean = false` - (_optional - see [Singletons](#singletons)_). ```js @@ -44,7 +44,7 @@ container.singleton('api_client', ApiClient); ``` The `singletonIf()` method is similar to `bindIf()`. It will only register a "shared" binding, if one has not already -registered. +been registered. ```js container.singletonIf('api_client', ApiClient); @@ -97,7 +97,7 @@ container.bind(STORAGE, CookieStorage); ## `concrete` Types The `concrete` argument for the `bind()`, `bindIf()`, `singleton()` and `singletonIf()` methods accepts -either of the following kind of value: +the following types: * Class constructor * "Factory" callback @@ -138,7 +138,8 @@ container.bind('recorder', (container) => { }); ``` -Although the above example shows an object instance being returned by the factory callback, any kind of value can be returned. +Although the above example shows an object instance being returned by the factory callback, any kind of value can be returned, +by the "factory" callback. ```js container.bind('my_message', () => 'Hi there...'); From 00cabcc03769811557599cf28387723656f206fe Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 8 Apr 2024 12:12:04 +0200 Subject: [PATCH 215/224] Add guide for defining dependencies on class constructors --- .../packages/container/dependencies.md | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/archive/current/packages/container/dependencies.md b/docs/archive/current/packages/container/dependencies.md index 320cde49..ea19a97e 100644 --- a/docs/archive/current/packages/container/dependencies.md +++ b/docs/archive/current/packages/container/dependencies.md @@ -5,4 +5,59 @@ sidebarDepth: 0 # Dependencies -// TODO: ... \ No newline at end of file +In order for the Service Container to be able to automatically inject dependencies, when [resolving](./resolving.md) +components, you must first define them on a target class. The `dependencies()` decorator is used for this purpose. + +```js +import { dependencies } from "@aedart/support/container"; + +@dependencies('engine') +export default class Car +{ + engine = undefined; + + constructor(engine) { + this.engine = engine; + } +} +``` + +::: tip No automatic injection +The `dependencies()` decorator _**does not automatically inject**_ anything into your class. +It will only associate binding identifiers with the target class, as [metadata](../support/meta). +This means that you can instantiate a new instance of the class, without any side effects (_dependencies must +be manually given as arguments to the target class_). + +```js +const car = new Car(); +console.log(car.engine); // undefined +``` + +The Service Container's [`make()` method](./resolving.md#the-make-method) is responsible for reading the defined +dependencies, resolve them, and inject them into the target class. +::: + +## Multiple Dependencies + +The `dependencies()` decorator accepts an arbitrary amount of binding identifiers. This allows you to define multiple +dependencies in a single call. + +```js +@dependencies( + 'warehouse_manager', + 'api_client', + 'events' +) +export default class Warehouse +{ + manager = undefined; + apiClient = undefined; + eventDispatcher = undefined; + + constructor(manager, apiClient, dispatcher) { + this.manager = manager; + this.apiClient = apiClient; + this.eventDispatcher = dispatcher; + } +} +``` \ No newline at end of file From 85482eaa06b8d79c6064318420571bfb57d295be Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 8 Apr 2024 14:34:13 +0200 Subject: [PATCH 216/224] Improve sentence --- docs/archive/current/packages/container/bindings.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/archive/current/packages/container/bindings.md b/docs/archive/current/packages/container/bindings.md index 665f2819..67b38c94 100644 --- a/docs/archive/current/packages/container/bindings.md +++ b/docs/archive/current/packages/container/bindings.md @@ -138,8 +138,8 @@ container.bind('recorder', (container) => { }); ``` -Although the above example shows an object instance being returned by the factory callback, any kind of value can be returned, -by the "factory" callback. +Although the above example shows an object instance being returned by the factory callback, any kind of value can be returned +by the callback. ```js container.bind('my_message', () => 'Hi there...'); From dc040920b00655e1de08a0265f38489117b2b0e7 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 8 Apr 2024 15:19:36 +0200 Subject: [PATCH 217/224] Add guide on how to resolve components and values from container --- .../current/packages/container/resolving.md | 215 +++++++++++++++++- 1 file changed, 213 insertions(+), 2 deletions(-) diff --git a/docs/archive/current/packages/container/resolving.md b/docs/archive/current/packages/container/resolving.md index d9c01275..5fa701be 100644 --- a/docs/archive/current/packages/container/resolving.md +++ b/docs/archive/current/packages/container/resolving.md @@ -5,8 +5,219 @@ sidebarDepth: 0 # Resolving -// TODO: ... +[[TOC]] ## The `make()` method -// TODO: ... \ No newline at end of file +To resolve component instances or values from the Service Container, use the `make()` method. +It accepts the following arguments: + +* `identifier: Identifier` - Target [binding identifier](./bindings.md#identifiers). +* `args: any[] = []` - (_optional_) Eventual arguments to be passed on to [class constructor](./bindings.md#constructors) or ["factory" callback](./bindings.md#factory-callbacks). + +```js +const recorder = container.make('recorder'); +``` + +When specifying a class constructor as the `identifier` argument, the `make()` method will automatically attempt to +create a new instance of the given class, even if no binding was registered for it. + +```js +class AudioPlayer +{ + // ...not shown... +} + +const audio = container.make(AudioPlayer); // new AudioPlayer instance +``` + +### Dependencies + +If the target that must be resolved is a class that has [dependencies defined](./dependencies.md) as [metadata](../support/meta), +then the `make()` method will automatically resolve them, and inject them into the target class. + +```js +import { dependencies } from "@aedart/support/container"; + +@dependencies('storage') +class TextRecorder +{ + storage = undeinfed; + + constructor(storage) { + this.storage = storage; + } +} + +// Register binding in the Service Container +container.singleton('storage', () => { + return new CookieStorage(); +}); + + +// ...Later in your application +const recorder = container.make(TextRecorder); +console.log(recorder.storage); // CookieStorage +``` + +### The `args` Argument + +You can also manually specify what arguments a class constructor or "factory" callback should receive, via the `args` argument. + +```js +const recorder = container.make(TextRecorder, [ new CloudStorage() ]); +console.log(recorder.storage); // CloudStorage +``` + +::: warning +When specifying the `args` argument for `make()`, any defined dependencies are **overwritten** by the values +in the `args` array, if a class constructor is requested resolved! +In other words, the binding identifiers defined via the [`dependencies` decorator](./dependencies.md) are ignored. +::: + +## The `call()` method + +The Service Container can also be used to invoke class methods or callbacks. This allows you to resolve a method's dependencies +and inject them. +The `call()` method accepts the following arguments: + +* `method: Callback | CallbackWrapper | ClassMethodReference` - The target callback or class method to invoke. +* `args: any[] = []` - (_optional_) Eventual arguments to be passed on to class method or callback. + +```js +class UsersRepository +{ + @dependencies('users_api_service') + fetchUser(usersService) + { + // ...not shown... + } +} + +// Later in your application +const promise = container.call([UsersRepository, 'fetchUser']); +``` + +### Class Method Reference + +A "class method reference" is an array that holds two values: + +* A class constructor or object instance. +* The name of the method to be invoked in the target class. + +```js +const reference = [AudioPlayer, 'play']; +``` + +When given as the `method` argument, for `call()`, the target class constructor is automatically resolved (_instantiated with eventual dependencies injected_). +The method is thereafter invoked and output is returned. +If the class method has any dependencies defined, then those will be resolved and injected into the method as arguments. + +```js +class AudioPlayer +{ + @dependencies('audio_processor', 'my_song') + play(processor, song) { + // ...play logic not shown... + return this; + } +} + +const player = container.call([AudioPlayer, 'play']); +``` + +::: warning +If you specify the `args` argument for `call()`, then eventual defined dependencies are **overwritten** with the values +provided in the `args` array. Thus, the dependencies of the class method are ignored. + +```js +const player = container.call( + [AudioPlayer, 'play'], + + // Arguments passed on to "play" method. + [ + new AudioProcessor(), + new FavouriteSong() + ] +); +``` +::: + +### Callback Wrapper + +When specifying a [callback wrapper](../support/CallbackWrapper.md) as target for `call()`, then the callback will be +invoked and eventual output is returned. If the wrapper has arguments specified, then they will automatically be applied, +the underlying callback is invoked. + +::: warning +Providing the `args` argument for `call()` will **overwrite** eventual arguments set in the callback wrapper! + +```js +import { CallbackWrapper } from "@aedart/support"; + +const wrapped = CallbackWrapper.make((firstname, lastname) => { + return `Hi ${firstname} ${lastname}`; +}, 'Brian', 'Jackson'); + +const result = container.call(wrapped, [ 'James', 'Brown' ]); +console.log(result); // Hi James Brown +``` +::: + +To define dependencies for a callback wrapper, you must use the wrapper's `set()` method and specify an array of target +binding identifiers for the `DEPENDENCIES` symbol as key. + +```js +import { DEPENDENCIES } from "@aedart/contracts/container"; + +const wrapped = CallbackWrapper.make((apiClient) => { + // ...fetch user logic not shown... + + return promise; +}).set(DEPENDENCIES, [ 'api_client' ]); + +const promise = container.call(wrapped); // Api Client injected into callback... +``` + +### Callback + +The `call()` can also be used for invoking a regular callback. Any `args` argument given to `call()` are passed on to +the callback, and eventual output value is returned. + +```js +const result = container.call((x) => { + return x * 2; +}, 4); + +console.log(result); // 8 +``` + +::: warning Limitation +At the moment, it is not possible to associate dependencies with a native callback directly. +Please use a [callback wrapper](#callback-wrapper) instead, if you need to inject dependencies into a callback. +::: + +## Hooks + +If you need to react to components or values that are being resolved from the Service Container, then you can use the +`before()` and `after()` hook methods. + +### `before()` + +The `before()` method registers a callback to be invoked before a binding is resolved. + +```js +container.before('user', (identifier, args, container) => { + // ...not shown... +}); +``` + +### `after()` + +The `after()` method registers a callback to be invoked after a binding has been resolved + +```js +container.after('user', (identifier, resolved, container) => { + // ...not shown... +}); +``` \ No newline at end of file From 6c38127b108b5622b15f9385e6ecac66a6ea9a19 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 8 Apr 2024 15:43:08 +0200 Subject: [PATCH 218/224] A guide for contextual bindings --- .../packages/container/contextual-bindings.md | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/docs/archive/current/packages/container/contextual-bindings.md b/docs/archive/current/packages/container/contextual-bindings.md index 01a277db..59314608 100644 --- a/docs/archive/current/packages/container/contextual-bindings.md +++ b/docs/archive/current/packages/container/contextual-bindings.md @@ -5,4 +5,66 @@ sidebarDepth: 0 # Contextual Bindings -// TODO: ... \ No newline at end of file +In situations when multiple classes make use of the same dependency, but you wish to inject a different component or +value on some of those classes, then you can make use of "context binding". +The `when()` method allows you to specify (_overwrite_) the implementation to be resolved and injected, for a given +target class. + +```js +container.when(ApiService) + .needs('storage') + .give(CookieStorage); + +container.when(UsersRepository, BooksRepository) + .needs('api_client') + .give(() => { + return new AcmeApiClient(); + }); +``` + +To illustrate the usefulness of contextual binding a bit further, consider the following example: + +```js +@dependency('storage') +class A { + // ...not shown... +} + +@dependency('storage') +class B { + // ...not shown... +} + +@dependency('storage') +class C { + // ...not shown... +} + +@dependency('storage') +class D { + // ...not shown... +} + +// Register "default" storage binding +container.singleton('storage', CookieStorage); + +// Register contextual binding for C and D +container.when(C, D) + .needs('storage') + .give(() => { + return new CloudStorage('s3'); + }); +``` + +In the above shown example, all classes define the same binding identifier (_"storage"_) as a dependency. +By default, a "storage" binding is registered in the Service Container, which ensures that when the classes are resolved, +a `CookieStorage` component instance is injected into each target class instance. + +However, classes `C` and `D` require a different implementation, than the one offered by the "storage" binding. +To achieve this, and without overwriting the default "storage" binding, a new contextual binding is registered that affects +only classes `C` and `D`. When they are resolved, a different implementation of injected into the target classes. + +```js +const c = container.make(C); +console.log(c.storage); // CloudStorage +``` \ No newline at end of file From 95becf5c1e9a1da6692410ae5948fb2b76f0b29a Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 8 Apr 2024 16:20:29 +0200 Subject: [PATCH 219/224] Add Facades doc (incomplete) --- .../packages/support/facades/README.md | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/archive/current/packages/support/facades/README.md diff --git a/docs/archive/current/packages/support/facades/README.md b/docs/archive/current/packages/support/facades/README.md new file mode 100644 index 00000000..f606f166 --- /dev/null +++ b/docs/archive/current/packages/support/facades/README.md @@ -0,0 +1,101 @@ +--- +title: About Facades +description: A static interface to classes +sidebarDepth: 0 +--- + +# Introduction + +The `@aedart/support/facades` package is an adaptation of [Laravel's Facades](https://laravel.com/docs/11.x/facades) +(_originally licensed under [MIT](https://github.com/laravel/framework/blob/11.x/src/Illuminate/Container/LICENSE.md)_). A [Facade](https://en.wikipedia.org/wiki/Facade_pattern) acts as an interface +to an underlying object instance. + +```js +import { Container } from "@aedart/support/facades"; + +const service = Container.obtain().make('api_service'); +``` + +[[TOC]] + +## Setup Facade's Service Container instance + +Before you can make use of facades, you must ensure that the `Facade` abstraction has a service container instance set. +This can be done via the static `setContainer()` method. + +```js +import { Container } from "@aedart/container"; +import { Facade } from "@aedart/support/facades"; + +// Somewhere in your application's setup or boot logic... +Facade.setContainer(Container.getInstance()); +``` + +Consequently, if you need to unset the Service Container instance and make sure that the `Facade` abstraction is cleared +of any previously resolved object instances, invoke the static `destroy()` method. + +```js +Facade.destroy(); +``` + +## Define a Facade + +To define your own Facade, extend the abstract `Facade` class. Specify the target [binding identifier](../../container/bindings.md#identifiers), +and the `obtain()` method. + +```js +import { Facade } from "@aedart/support/facades"; + +export default class ApiFacade extends Facade +{ + static getIdentifier() + { + return 'api_client'; + } + + /** + * @inheritDoc + * + * @return {import('@acme/api').ApiClient} + */ + static obtain() + { + return this.resolveIdentifier(); + } +} +``` + +### The `obtain()` method + +Since a facade is only an "interface" to an underlying object instance, that has been registered in a [Service Container](../../container/README.md), +you must specify how that object instance must be resolved. Typically, invoking the internal `resolveIdentifier()` will be sufficient. +However, in special circumstances, you might need to resolve a binding differently, or perhaps perform +some kind of additional post-resolve logic. + +```js +export default class LimitedApiFacade extends Facade +{ + static getIdentifier() + { + return 'api_client'; + } + + /** + * @inheritDoc + * + * @return {import('@acme/api').ApiClient} + */ + static obtain() + { + const client = this.resolveIdentifier(); + client.error_response_thresshold = 3; + client.ttl = 350; + + return client; + } +} +``` + +## Testing + +TODO: .... \ No newline at end of file From ab220485b83fbd4c474b1456f7cf629963b80ac6 Mon Sep 17 00:00:00 2001 From: alin Date: Mon, 8 Apr 2024 16:20:48 +0200 Subject: [PATCH 220/224] Add support/facades collection --- docs/.vuepress/archive/Version0x.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index 27ba6975..b77c55c6 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -91,6 +91,13 @@ export default PagesCollection.make('v0.x', '/v0x', [ 'packages/support/exceptions/customErrors', ] }, + { + text: 'Facades', + collapsible: true, + children: [ + 'packages/support/facades/', + ] + }, { text: 'Meta', collapsible: true, From f7f04b0e7bde7a88cb27e265a559bacbce7e8717 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 9 Apr 2024 10:36:18 +0200 Subject: [PATCH 221/224] Refactor Facade, remove the need for resolveIdentifier() Now, each TypeScript implementation of a Facade can simply just declare the return type of obtain via the internal "type" property. This seems much cleaner. --- packages/support/src/facades/Container.ts | 31 +++++++++---- packages/support/src/facades/Facade.ts | 44 +++++++------------ .../packages/support/facades/Facade.test.js | 19 -------- 3 files changed, 37 insertions(+), 57 deletions(-) diff --git a/packages/support/src/facades/Container.ts b/packages/support/src/facades/Container.ts index 4e8aa85c..e6429c83 100644 --- a/packages/support/src/facades/Container.ts +++ b/packages/support/src/facades/Container.ts @@ -7,18 +7,31 @@ import Facade from "./Facade"; */ export default class Container extends Facade { - public static getIdentifier(): Identifier - { - return CONTAINER; - } - /** - * @inheritDoc + * The "type" of the resolved object instance. + * + * **Note**: _This property is not used for anything other + * than to provide a TypeScript return type for the `obtain()` + * method._ + * + * @type {ServiceContainer} * - * @return {import('@aedart/contracts/container').Container} + * @protected + * @static */ - public static obtain() + protected static type: ServiceContainer; + + /** + * Returns identifier to be used for resolving facade's underlying object instance + * + * @return {Identifier} + * + * @abstract + * + * @static + */ + public static getIdentifier(): Identifier { - return this.resolveIdentifier(); + return CONTAINER; } } \ No newline at end of file diff --git a/packages/support/src/facades/Facade.ts b/packages/support/src/facades/Facade.ts index c91651c7..0fd9da54 100644 --- a/packages/support/src/facades/Facade.ts +++ b/packages/support/src/facades/Facade.ts @@ -24,6 +24,18 @@ export default abstract class Facade */ protected static container: Container | undefined = undefined; + /** + * The "type" of the resolved object instance. + * + * **Note**: _This property is not used for anything other + * than to provide a TypeScript return type for the `obtain()` + * method._ + * + * @protected + * @static + */ + protected static type: any; + /** * Resolved instances * @@ -68,9 +80,9 @@ export default abstract class Facade */ public static getIdentifier(): Identifier { - throw new LogicalError('Facade does not implement the getAccessor() method'); + throw new LogicalError('Facade does not implement the getIdentifier() method'); } - + /** * Obtain the underlying object instance, or a "spy" (for testing) * @@ -88,10 +100,7 @@ export default abstract class Facade */ public static obtain() { - // Use this method to resolve the binding from the service container. - // E.g. return this.resolveIdentifier(); - - throw new LogicalError('Facade does not implement the get() method'); + return this.resolve(this.getIdentifier()); } /** @@ -294,29 +303,6 @@ export default abstract class Facade this.forgetAllResolved(); this.forgetContainer(); } - - /** - * Resolves the facade's underlying object instance from the service container. - * - * @see resolve - * - * @template T = any - * - * @return {T} - * - * @throws {NotFoundException} - * @throws {ContainerException} - * @throws {LogicalError} - * - * @protected - * @static - */ - protected static resolveIdentifier< - T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ - >(): T - { - return this.resolve(this.getIdentifier()); - } /** * Resolves the facade's underlying object instance from the service container, diff --git a/tests/browser/packages/support/facades/Facade.test.js b/tests/browser/packages/support/facades/Facade.test.js index 0e7d866c..43f8e238 100644 --- a/tests/browser/packages/support/facades/Facade.test.js +++ b/tests/browser/packages/support/facades/Facade.test.js @@ -31,18 +31,6 @@ describe('@aedart/support/facades', () => { .toThrowError(LogicalError); }); - it('fails when obtain() not implemented', () => { - - class MyFacade extends Facade {} - - const callback = () => { - return MyFacade.obtain(); - } - - expect(callback) - .toThrowError(LogicalError); - }); - it('can set and get service container', () => { const container = new Container(); @@ -147,13 +135,6 @@ describe('@aedart/support/facades', () => { { return 'my_identifier'; } - - /** - * @return {Foo} - */ - static obtain() { - return this.resolveIdentifier(); - } } class Foo { From ed3242a1934cd2c60e4d7af5bc600bb28697f48a Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 9 Apr 2024 10:48:41 +0200 Subject: [PATCH 222/224] Fix JSDoc type --- packages/support/src/facades/Container.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/support/src/facades/Container.ts b/packages/support/src/facades/Container.ts index e6429c83..00349891 100644 --- a/packages/support/src/facades/Container.ts +++ b/packages/support/src/facades/Container.ts @@ -14,7 +14,7 @@ export default class Container extends Facade * than to provide a TypeScript return type for the `obtain()` * method._ * - * @type {ServiceContainer} + * @type {import('@aedart/contracts/container').Container} * * @protected * @static From dfafaa504b844ee4ca1e651fe807ba6da99b5136 Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 9 Apr 2024 11:49:46 +0200 Subject: [PATCH 223/224] Refine Facades docs --- .../packages/support/facades/README.md | 135 +++++++++++++++--- 1 file changed, 114 insertions(+), 21 deletions(-) diff --git a/docs/archive/current/packages/support/facades/README.md b/docs/archive/current/packages/support/facades/README.md index f606f166..2047bad9 100644 --- a/docs/archive/current/packages/support/facades/README.md +++ b/docs/archive/current/packages/support/facades/README.md @@ -7,8 +7,8 @@ sidebarDepth: 0 # Introduction The `@aedart/support/facades` package is an adaptation of [Laravel's Facades](https://laravel.com/docs/11.x/facades) -(_originally licensed under [MIT](https://github.com/laravel/framework/blob/11.x/src/Illuminate/Container/LICENSE.md)_). A [Facade](https://en.wikipedia.org/wiki/Facade_pattern) acts as an interface -to an underlying object instance. +(_originally licensed under [MIT](https://github.com/laravel/framework/blob/11.x/src/Illuminate/Container/LICENSE.md)_). In this context, a [Facade](https://en.wikipedia.org/wiki/Facade_pattern) acts as an interface (_or gateway_) to an +underlying object instance, resolved from the [Service Container](../../container/README.md). ```js import { Container } from "@aedart/support/facades"; @@ -40,8 +40,7 @@ Facade.destroy(); ## Define a Facade -To define your own Facade, extend the abstract `Facade` class. Specify the target [binding identifier](../../container/bindings.md#identifiers), -and the `obtain()` method. +To define your own Facade, extend the abstract `Facade` class, and specify the target [binding identifier](../../container/bindings.md#identifiers). ```js import { Facade } from "@aedart/support/facades"; @@ -52,25 +51,34 @@ export default class ApiFacade extends Facade { return 'api_client'; } +} +``` - /** - * @inheritDoc - * - * @return {import('@acme/api').ApiClient} - */ - static obtain() +If you are using TypeScript, then you can also specify the return type of the `obtain()` method, by declaring the +underlying resolved object's type, for the internal `type` property (_`type` property is not used for any other purpose_). + +```ts +import type { Identifier } from "@aedart/contracts/container"; +import { Facade } from "@aedart/support/facades"; +import type { AcmeApiClient } from "@acme/contracts/api"; + +export default class ApiFacade extends Facade +{ + protected static type: AcmeApiClient; + + public static getIdentifier(): Identifier { - return this.resolveIdentifier(); + return 'api_client'; } } ``` -### The `obtain()` method +## The `obtain()` method -Since a facade is only an "interface" to an underlying object instance, that has been registered in a [Service Container](../../container/README.md), -you must specify how that object instance must be resolved. Typically, invoking the internal `resolveIdentifier()` will be sufficient. -However, in special circumstances, you might need to resolve a binding differently, or perhaps perform -some kind of additional post-resolve logic. +The `obtain()` is used to obtain the Facade's underlying object instance. Typically, you do not need to do anything more +than to implement the `getIdentifier()` method in your concrete facade class. +But, in some situations you might need to resolve a binding differently. Or, perhaps perform some kind of additional +post-resolve logic, in order to make easier / simpler to work with the resolved object. ```js export default class LimitedApiFacade extends Facade @@ -81,13 +89,11 @@ export default class LimitedApiFacade extends Facade } /** - * @inheritDoc - * - * @return {import('@acme/api').ApiClient} + * @return {import('@acme/contracts/api').AcmeApiClient} */ static obtain() { - const client = this.resolveIdentifier(); + const client = this.resolve(this.getIdentifier()); client.error_response_thresshold = 3; client.ttl = 350; @@ -96,6 +102,93 @@ export default class LimitedApiFacade extends Facade } ``` +```js +const promise = LimitedApiFacade.obtain().fetch('https://acme.com/api/users'); +``` + ## Testing -TODO: .... \ No newline at end of file +When you need to test components that rely on Facades, you can register a "spy" (_mocked object_), via the static +method `spy()`. Consider, for instance, that you have a users repository component that relies on a custom Api facade. + +```js +import { ApiFacade } from "@acme/facades"; + +class UsersRepository { + + fetch() { + return ApiFacade.obtain().fetch('https://acme.com/api/users'); + } + + // ...remaining not shown... +} +``` + +In your testing environment, you can specify a callback that can be used to create a fake object (_mocked object_) that +must behave in a certain way, via the `spy()` method. The callback must return either of the following: + +* The Facade's underlying resolved object. +* Or, a fake object that behaves as desired (_in the context of your test_). + +```js +ApiFacade.spy((container, identifier) => { + // ...mocking not shown ... + + return myResolvedObject; // resolved or mocked object +}); +``` + +All subsequent calls to the facade's underlying object will be made to the registered "spy" object instead. + +The following example uses [Jasmine](https://jasmine.github.io/) as testing framework. +However, the `spy()` method is not tied to any specific testing or object mocking framework. Feel free to use whatever +testing tools or frameworks fits your purpose best. + +```js +import { ApiFacade } from "@acme/facades"; +import { UsersRepository } from "@app"; + +// E.g. testing via Jasmine Framework... +describe('@acme/api', () => { + + // Test setup not shown in this example... + + afterEach(() => { + Facade.destroy(); + }); + + it('can obtain users', () => { + + let mocked = null; + ApiFacade.spy((container, identifier) => { + const apiClient = container.get(identifier); + + mocked = spyOn(apiClient, 'fetch') + .and + .returnValue([ + { id: 12, name: 'Jackie' }, + { id: 14, name: 'Lana' }, + // ...etc + ]); + + // return the resolved api client... + return apiClient; + }); + + const repo = new UsersRepository(); + const users = repo.fetch(); + + expect(users) + .not + .toBeUndefined(); + + expect(mocked) + .toHaveBeenCalled(); + }); +}); +``` + +## Onward + +Please consider reading Laravel's ["When to Utilize Facades"](https://laravel.com/docs/11.x/facades#when-to-use-facades), +to gain an idea of when using Facades can be good, and when not. \ No newline at end of file From b862c579360be1afc74b9979af26a9a30775e21c Mon Sep 17 00:00:00 2001 From: alin Date: Tue, 9 Apr 2024 13:10:24 +0200 Subject: [PATCH 224/224] Highlight Service Container and Facades --- docs/archive/current/README.md | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/archive/current/README.md b/docs/archive/current/README.md index d57681aa..dc353931 100644 --- a/docs/archive/current/README.md +++ b/docs/archive/current/README.md @@ -29,6 +29,46 @@ _TBD: "To be decided"._ ## `v0.x` Highlights +### Service Container + +An adaptation of Laravel's Service Container that offers a way to with powerful tool to manage dependencies and perform +dependency injection. + +```js +import { Container } from "@aedart/container"; + +container.bind('storage', () => { + return new CloudService('s3'); +}); + +// Later in your application. +const storage = container.make('storage'); +``` + +For additional examples, see the [Service Container documentation](./packages/container/README.md). + +### Facades + +Adaptation of Laravel's Facade component. It acts as an interface or gateway to an underlying object that is resolved +from the Service Container. + +```js +import { Facade } from "@aedart/support/facades"; + +export default class ApiFacade extends Facade +{ + static getIdentifier() + { + return 'api_client'; + } +} + +// Later in your application +const promise = ApiFacade.obtain().fetch('https://acme.com/api/users'); +``` + +See the [Facades documentation](./packages/support/facades/README.md) for additional details. + ### Concerns Intended as an alternative to mixins, the [Concerns](./packages/support/concerns/README.md) submodule offers a different