diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d2a852..6387e586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,18 +9,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* Add upgrade guide from v0.7.x- to v0.10.x. +* 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`. +* `ClassMethodName` and `ClassMethodReference` type aliases in `@aedart/contracts`. +* `isMethod()` util in `@aedart/support/reflections`. +* `ConstructorLike` and `Callback` type aliases, in `@aedart/constracts`. +* `CallbackWrapper` util class, in `@aedart/support`. +* `isCallbackWrapper` util, in `@aedart/support`. +* `ArbitraryData` concern, in `@aedart/support`. +* `arrayMergeOptions` in object `merge()`. +* Add upgrade guide for "v0.7.x- to v0.10.x". ### Changed +**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** + * 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 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. +* 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 * 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 + +* `ConstructorOrAbstractConstructor` type alias. It has been replaced with the new `ConstructorLike` type., in `@aedart/constracts`. ## [0.10.0] - 2024-03-07 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 diff --git a/aliases.js b/aliases.js index b5f9cc7b..2034e3f5 100644 --- a/aliases.js +++ b/aliases.js @@ -18,10 +18,15 @@ 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'), '@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/docs/.vuepress/archive/Version0x.ts b/docs/.vuepress/archive/Version0x.ts index 93955b40..b77c55c6 100644 --- a/docs/.vuepress/archive/Version0x.ts +++ b/docs/.vuepress/archive/Version0x.ts @@ -22,6 +22,20 @@ 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/dependencies', + 'packages/container/resolving', + 'packages/container/contextual-bindings', + ] + }, { text: 'Contracts', collapsible: true, @@ -77,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, @@ -141,9 +162,13 @@ 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', 'packages/support/reflections/isKeySafe', 'packages/support/reflections/isKeyUnsafe', + 'packages/support/reflections/isMethod', 'packages/support/reflections/isSubclass', 'packages/support/reflections/isSubclassOrLooksLike', 'packages/support/reflections/isWeakKind', @@ -164,6 +189,7 @@ export default PagesCollection.make('v0.x', '/v0x', [ 'packages/support/misc/toWeakRef', ] }, + 'packages/support/CallbackWrapper', ] }, { 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 diff --git a/docs/archive/current/packages/container/README.md b/docs/archive/current/packages/container/README.md new file mode 100644 index 00000000..ee026d19 --- /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) +(_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 a 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 diff --git a/docs/archive/current/packages/container/bindings.md b/docs/archive/current/packages/container/bindings.md new file mode 100644 index 00000000..67b38c94 --- /dev/null +++ b/docs/archive/current/packages/container/bindings.md @@ -0,0 +1,184 @@ +--- +description: Service Container Bindings +sidebarDepth: 0 +--- + +# Bindings + +[[TOC]] + +## Basics + +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 from the container. +* `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 + +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 +been registered. + +```js +container.singletonIf('api_client', ApiClient); +``` + +### Instances + +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 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` +* `number` +* `object` (_not `null`_) +* Class Constructor +* Callback + +::: 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 + +The `concrete` argument for the `bind()`, `bindIf()`, `singleton()` and `singletonIf()` methods accepts +the following types: + +* Class constructor +* "Factory" callback + +### Constructors + +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). + +```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 +by the callback. + +```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 + +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 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..53df8252 --- /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 like any other regular class. 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 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. Otherwise, the method will return the existing instance. + +```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 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..59314608 --- /dev/null +++ b/docs/archive/current/packages/container/contextual-bindings.md @@ -0,0 +1,70 @@ +--- +description: Define Contextual Bindings +sidebarDepth: 0 +--- + +# Contextual Bindings + +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 diff --git a/docs/archive/current/packages/container/dependencies.md b/docs/archive/current/packages/container/dependencies.md new file mode 100644 index 00000000..ea19a97e --- /dev/null +++ b/docs/archive/current/packages/container/dependencies.md @@ -0,0 +1,63 @@ +--- +description: How to define dependencies +sidebarDepth: 0 +--- + +# Dependencies + +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 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 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 diff --git a/docs/archive/current/packages/container/resolving.md b/docs/archive/current/packages/container/resolving.md new file mode 100644 index 00000000..5fa701be --- /dev/null +++ b/docs/archive/current/packages/container/resolving.md @@ -0,0 +1,223 @@ +--- +description: Resolving Dependencies +sidebarDepth: 0 +--- + +# Resolving + +[[TOC]] + +## The `make()` method + +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 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 diff --git a/docs/archive/current/packages/support/arrays/merge.md b/docs/archive/current/packages/support/arrays/merge.md index c7a54b64..17055106 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 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..2047bad9 --- /dev/null +++ b/docs/archive/current/packages/support/facades/README.md @@ -0,0 +1,194 @@ +--- +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)_). 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"; + +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, and specify the target [binding identifier](../../container/bindings.md#identifiers). + +```js +import { Facade } from "@aedart/support/facades"; + +export default class ApiFacade extends Facade +{ + static getIdentifier() + { + return 'api_client'; + } +} +``` + +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 'api_client'; + } +} +``` + +## The `obtain()` method + +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 +{ + static getIdentifier() + { + return 'api_client'; + } + + /** + * @return {import('@acme/contracts/api').AcmeApiClient} + */ + static obtain() + { + const client = this.resolve(this.getIdentifier()); + client.error_response_thresshold = 3; + client.ttl = 350; + + return client; + } +} +``` + +```js +const promise = LimitedApiFacade.obtain().fetch('https://acme.com/api/users'); +``` + +## Testing + +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 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. 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 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 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..01b46eee --- /dev/null +++ b/docs/archive/current/packages/support/reflections/isClassConstructor.md @@ -0,0 +1,39 @@ +--- +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. +::: + +::: 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"; + +isClassConstructor(null); // false +isClassConstructor({}); // false +isClassConstructor([]); // false +isClassConstructor(function() {}); // false +isClassConstructor(() => {}); // false +isClassConstructor(Array); // false + +class A {} +isClassConstructor(A); // true +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). 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/docs/archive/current/packages/support/reflections/isConstructor.md b/docs/archive/current/packages/support/reflections/isConstructor.md index ae7b7ff4..acd9080e 100644 --- a/docs/archive/current/packages/support/reflections/isConstructor.md +++ b/docs/archive/current/packages/support/reflections/isConstructor.md @@ -6,20 +6,31 @@ 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** 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 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/package-lock.json b/package-lock.json index 77c5ef1f..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", @@ -77,6 +78,10 @@ "node": ">=0.10.0" } }, + "node_modules/@aedart/container": { + "resolved": "packages/container", + "link": true + }, "node_modules/@aedart/contracts": { "resolved": "packages/contracts", "link": true @@ -282,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", @@ -2520,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": { @@ -3038,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" @@ -4118,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" ], @@ -4131,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" ], @@ -4144,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" ], @@ -4157,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" ], @@ -4170,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" ], @@ -4183,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" ], @@ -4196,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" ], @@ -4209,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" ], @@ -4222,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" ], @@ -4235,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" ], @@ -4248,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" ], @@ -4261,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" ], @@ -4274,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" ], @@ -4572,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": { @@ -4631,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" } @@ -4753,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", @@ -4821,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": { @@ -4849,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" @@ -4866,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" }, @@ -4893,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" @@ -4906,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", @@ -5005,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": { @@ -5063,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": { @@ -5253,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", @@ -5305,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" }, @@ -5333,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" }, @@ -5371,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" }, @@ -5446,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": { @@ -5492,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", @@ -6231,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": { @@ -6257,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", @@ -6269,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", @@ -6708,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", @@ -8393,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", @@ -8511,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" @@ -10021,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" }, @@ -17502,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" @@ -17557,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" @@ -17927,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" @@ -17942,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" } }, @@ -18588,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" @@ -19508,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" @@ -20350,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", @@ -20702,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" @@ -20797,6 +20834,14 @@ "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": { "version": "0.10.0", "license": "BSD-3-Clause" @@ -20807,7 +20852,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" } }, "packages/vuepress-utils": { 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", 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..9d6c6e20 --- /dev/null +++ b/packages/container/NOTICE @@ -0,0 +1,44 @@ +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 +, 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. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +@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 diff --git a/packages/container/README.md b/packages/container/README.md new file mode 100644 index 00000000..00c771f4 --- /dev/null +++ b/packages/container/README.md @@ -0,0 +1,21 @@ +# Service Container + +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 a way to: + +* Manage class dependencies +* Perform [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) + +# 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..d7737616 --- /dev/null +++ b/packages/container/rollup.config.mjs @@ -0,0 +1,28 @@ +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/container', + '@aedart/contracts/support/concerns', + '@aedart/contracts/support/exceptions', + '@aedart/contracts/support/meta', + '@aedart/contracts/support/mixins', + '@aedart/contracts/support/reflections', + '@aedart/support', + '@aedart/support/arrays', + '@aedart/support/container', + '@aedart/support/concerns', + '@aedart/support/exceptions', + '@aedart/support/facades', + '@aedart/support/meta', + '@aedart/support/misc', + '@aedart/support/mixins', + '@aedart/support/reflections', + + 'lodash-es' + ] +}); diff --git a/packages/container/src/BindingEntry.ts b/packages/container/src/BindingEntry.ts new file mode 100644 index 00000000..aecd2876 --- /dev/null +++ b/packages/container/src/BindingEntry.ts @@ -0,0 +1,170 @@ +import type { + Binding, + FactoryCallback, + Identifier +} from "@aedart/contracts/container"; +import type { Constructor } from "@aedart/contracts"; +import { isConstructor } from "@aedart/support/reflections"; +import { isBindingIdentifier } from "@aedart/support/container"; + +/** + * Binding Entry + */ +export default class BindingEntry< + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +> implements Binding +{ + /** + * This binding's identifier + * + * @type {Identifier} + * + * @readonly + * + * @protected + */ + protected readonly _identifier: Identifier; + + /** + * The bound value to be resolved by a service container + * + * @template T = any + * + * @type {FactoryCallback | Constructor} + * + * @readonly + * + * @protected + */ + protected readonly _value: FactoryCallback | Constructor; + + /** + * Shared state of resolved value + * + * @type {boolean} If `true`, then service container must register resolved + * value as a singleton. + * + * @readonly + * + * @protected + */ + protected readonly _shared: boolean; + + /** + * State, whether value is a factory callback or not + * + * @type {boolean|null} + * + * @protected + */ + protected _isFactoryCallback: boolean|null = null; + + /** + * State, whether value is a constructor or not + * + * @type {boolean|null} + * + * @protected + */ + _isConstructor: boolean|null = null; + + /** + * Create new Binding Entry instance + * + * @template T = any + * + * @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; + + 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; + + 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/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/Builder.ts b/packages/container/src/Builder.ts new file mode 100644 index 00000000..e61e4c6a --- /dev/null +++ b/packages/container/src/Builder.ts @@ -0,0 +1,83 @@ +import type { + Container, + ContextualBindingBuilder, + FactoryCallback, + Identifier +} from "@aedart/contracts/container"; +import type { Constructor } from "@aedart/contracts"; + +/** + * 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[]} + * + * @protected + */ + protected concrete: 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[]} concrete + */ + constructor(container: Container, ...concrete: 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 + { + 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 new file mode 100644 index 00000000..f8ce4b07 --- /dev/null +++ b/packages/container/src/Container.ts @@ -0,0 +1,1331 @@ +import { + AfterResolvedCallback, + Alias, + BeforeResolvedCallback, + Container as ServiceContainerContract, + ContextualBindingBuilder, + ExtendCallback, + FactoryCallback, + Identifier, + Binding, + ReboundCallback, +} from "@aedart/contracts/container"; +import type { + Callback, + ClassMethodName, + ClassMethodReference, + Constructor, + 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 { + isConstructor, + isClassMethodReference, + getNameOrDesc, + hasAllMethods, +} from "@aedart/support/reflections"; +import { + isCallbackWrapper, + CallbackWrapper as Wrapper +} from "@aedart/support"; +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"; + +/** + * 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 +{ + /** + * Singleton instance of the service container + * + * @type {ServiceContainerContract|null} + * + * @protected + * + * @static + */ + protected 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(); + + /** + * Extend callbacks + * + * @type {Map} + * + * @protected + */ + protected extenders: Map = new Map(); + + /** + * Resolved (built) identifiers + * + * @type {Set} + * + * @protected + */ + protected resolved: Set = new Set(); + + /** + * Contextual Bindings + * + * @type {Map>} + * + * @protected + */ + protected contextualBindings: Map< + Constructor, + Map + > = new Map(); + + /** + * "Before" resolved callbacks + * + * @type {Map} + * + * @protected + */ + protected beforeResolvedCallbacks: Map = new Map(); + + /** + * "After" resolved callbacks + * + * @type {Map} + * + * @protected + */ + protected afterResolvedCallbacks: Map = new Map(); + + /** + * Rebound callbacks + * + * @type {Map} + * + * @protected + */ + protected reboundCallbacks: Map = new Map(); + + /** + * Resolve stack + * + * @type {Set} + * + * @protected + */ + protected resolveStack: Set = new Set(); + + /** + * Returns the singleton instance of the service container + * + * @return {ServiceContainerContract|this} + */ + public 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} + */ + public static setInstance(container: ServiceContainerContract | null = null): ServiceContainerContract | null + { + return this.instance = container; + } + + /** + * Register a binding + * + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} [concrete] + * @param {boolean} [shared=false] + * + * @returns {this} + * + * @throws {TypeError} + */ + public bind(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared: boolean = false): this + { + concrete = concrete ?? identifier as Constructor; + + this.bindings.set(identifier, this.makeBindingEntry(identifier, concrete, shared)); + + // 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; + } + + /** + * 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} + */ + public bindIf(identifier: Identifier, concrete?: FactoryCallback | Constructor, shared: boolean = false): this + { + if (!this.bound(identifier)) { + this.bind(identifier, concrete, shared); + } + + return this; + } + + /** + * Register a shared binding + * + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} [concrete] + * + * @returns {this} + * + * @throws {TypeError} + */ + public singleton(identifier: Identifier, concrete?: FactoryCallback | Constructor): this + { + return this.bind(identifier, concrete, true); + } + + /** + * Register a shared binding, if none already exists for given identifier + * + * @param {Identifier} identifier + * @param {FactoryCallback | Constructor} [concrete] + * + * @returns {this} + * + * @throws {TypeError} + */ + public singletonIf(identifier: Identifier, concrete?: FactoryCallback | Constructor): this + { + if (!this.bound(identifier)) { + this.singleton(identifier, concrete); + } + + return this; + } + + /** + * Register existing object instance as a shared binding + * + * @template T = object + * + * @param {Identifier} identifier + * @param {T} instance + * + * @returns {T} + * + * @throws {TypeError} + */ + public instance(identifier: Identifier, instance: T): T + { + const isBound: boolean = this.has(identifier); + + this.instances.set(identifier, instance as object); + + // If the identifier was already bound before, invoke the rebound callbacks. + if (isBound) { + this.rebound(identifier); + } + + 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[]} concrete + * + * @return {ContextualBindingBuilder} + * + * @throws {TypeError} + */ + public when(...concrete: Constructor[]): ContextualBindingBuilder + { + return new Builder(this, ...concrete); + } + + /** + * 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 + * + * @return {boolean} + */ + public hasContextualBinding(target: Constructor, identifier: Identifier): boolean + { + return this.hasContextualBindings(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 + * + * @template T = any + * + * @param {Identifier} identifier + * + * @returns {T} + * + * @throws {NotFoundException} + * @throws {ContainerException} + */ + public get< + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(identifier: Identifier): T + { + return this.make(identifier); + } + + /** + * Determine if an entry is registered for given identifier. + * + * @param {Identifier} identifier + * + * @returns {boolean} + */ + public has(identifier: Identifier): boolean + { + return this.bindings.has(identifier) + || this.instances.has(identifier) + || this.isAlias(identifier); + } + + /** + * Alias for {@link has} + * + * @param {Identifier} identifier + * + * @returns {boolean} + */ + public bound(identifier: Identifier): boolean + { + return this.has(identifier); + } + + /** + * Alias identifier as a different identifier + * + * @param {Identifier} identifier + * @param {Alias} alias + * + * @returns {this} + * + * @throws {TypeError} + */ + public alias(identifier: Identifier, alias: Alias): 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} + */ + public isAlias(identifier: Identifier): boolean + { + 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 identifier for given alias, if available + * + * @param {Identifier} alias + * + * @returns {Identifier} + */ + public getAlias(alias: Identifier): Identifier + { + return this.aliases.has(alias) + ? this.getAlias(this.aliases.get(alias) as Identifier) + : alias; + } + + /** + * 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} + */ + 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 + { + return this.resolve(identifier, args); + } + + /** + * 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} + */ + 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 + { + if (!this.has(identifier) && !this.isBuildable(identifier)) { + if (typeof defaultValue === 'function') { + return defaultValue(this, args); + } + + return defaultValue as D; + } + + return this.make(identifier, args); + } + + /** + * Instantiate a new instance of given concrete + * + * @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, + args: any[] = [] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): T + { + const isBinding: boolean = this.isBinding(concrete); + let identifier: Identifier = concrete; + + // 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)) { + 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. + return this.resolveConstructor( + concrete as Constructor, + args, + identifier + ); + } + + /** + * Call given method and inject dependencies if needed + * + * @param {Callback | CallbackWrapper | ClassMethodReference} method + * @param {any[]} [args] + * + * @return {any} + * + * @throws {ContainerException} + */ + public call(method: Callback | CallbackWrapper | ClassMethodReference, args: any[] = []): any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + { + 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 } }); + } + + /** + * Extend the registered binding + * + * @param {Identifier} identifier + * @param {ExtendCallback} callback + * + * @return {this} + * + * @throws {TypeError} + * @throws {ContainerException} + */ + public extend(identifier: Identifier, callback: ExtendCallback): 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)); + + this.rebound(identifier); + } 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); + + 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 + * + * @param {Identifier} identifier + * + * @returns {boolean} + */ + public forget(identifier: Identifier): boolean + { + const removedAlias: boolean = this.aliases.delete(identifier); + const removedInstance: boolean = this.forgetInstance(identifier); + const removedBinding: boolean = this.bindings.delete(this.getAlias(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 + * + * @returns {void} + */ + public flush(): void + { + this.bindings.clear(); + this.instances.clear(); + this.aliases.clear(); + this.resolved.clear(); + this.contextualBindings.clear(); + this.resolveStack.clear(); + } + + /** + * Determine if identifier has been resolved + * + * @param {Identifier} identifier + * + * @return {boolean} + */ + public isResolved(identifier: Identifier): boolean + { + identifier = this.getAlias(identifier); + + return this.resolved.has(identifier) + || this.instances.has(identifier); + } + + /** + * Register a callback to be invoked before a binding is resolved + * + * @param {Identifier} identifier + * @param {BeforeResolvedCallback} callback + * + * @return {this} + */ + public before(identifier: Identifier, callback: BeforeResolvedCallback): 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; + } + + /** + * Register a callback to be invoked after a binding has been resolved + * + * @param {Identifier} identifier + * @param {AfterResolvedCallback} callback + * + * @return {this} + */ + public after(identifier: Identifier, callback: AfterResolvedCallback): 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; + } + + /** + * 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; + } + + /** + * 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 + ): T + { + try { + return callback(this, ...args) as T; + } catch (e) { + identifier = identifier ?? callback; + + 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 + ): T + { + try { + 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 and resolve them from the container. + if (args.length == 0 && this.hasDependencies(target)) { + args = this.resolveDependencies(target); + } + + // Create the instance with arguments. + const resolved: T = new target(...args) as T; + + this.resolveStack.delete(target as Constructor); + + return resolved; + } 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: { + 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 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]; + + // Resolve evt. dependencies if no arguments are given... + if (args.length == 0 && this.hasDependencies(method as object)) { + args = this.resolveDependencies(method as object); + } + + // Wrap the class method into a callback-wrapper, such that the "ThisArg" can be + // applied correctly. + return this.invokeCallbackWrapper( + Wrapper.makeFor(target, method, 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-expect-error TS7053 - has method is in wrapper at this point */ + && wrapper['has'](DEPENDENCIES) + ) { + /* @ts-expect-error TS7053 - get method is in wrapper at this point */ + 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)); + } + + 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 (if any dependencies are defined for the callback). + 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 + * + * @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 { + // 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) { + 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; + } + + 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 "rebound" callbacks for given identifier + * + * @param {Identifier} identifier + * + * @return {void} + * + * @throws {ContainerError} + * + * @protected + */ + protected rebound(identifier: Identifier): void + { + const resolved = this.make(identifier); + + const callbacks: ReboundCallback[] = this.getReboundCallbacks(identifier); + for (const callback of callbacks) { + callback(resolved, this); + } + } + + /** + * 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 + * + * @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); + } + + /** + * 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 + * + * @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); + } + + /** + * Aborts current make, build or call process, if given target is already in + * the process of being resolved. + * + * @param {Constructor} target + * + * @throws {CircularDependencyError} + * + * @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); + }); + stack.push(getNameOrDesc(target)); + + const trace: string = stack.join(' -> '); + + 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/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..edf4c86d --- /dev/null +++ b/packages/container/src/exceptions/NotFoundError.ts @@ -0,0 +1,24 @@ +import type { NotFoundException } from "@aedart/contracts/container"; +import ContainerError from "./ContainerError"; +import { configureCustomError } from "@aedart/support/exceptions"; + +/** + * 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); + + configureCustomError(this); + } +} \ 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..db9803db --- /dev/null +++ b/packages/container/src/exceptions/index.ts @@ -0,0 +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/container/src/index.ts b/packages/container/src/index.ts new file mode 100644 index 00000000..cc16885b --- /dev/null +++ b/packages/container/src/index.ts @@ -0,0 +1,12 @@ +import Builder from "./Builder"; +import BindingEntry from "./BindingEntry"; +import Container from "./Container"; +export { + Builder, + BindingEntry, + Container +} + +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/packages/container/tsconfig.json b/packages/container/tsconfig.json new file mode 100644 index 00000000..463dd768 --- /dev/null +++ b/packages/container/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../shared/tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "declarationDir": "./dist/tmp", + "composite": true + }, + "include": [ + "./src/**/*" + ], + "references": [ + { "path": "../contracts", "prepend": false }, + { "path": "../support", "prepend": false }, + ] +} diff --git a/packages/contracts/NOTICE b/packages/contracts/NOTICE index f97cc79b..baf4cfe1 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 @@ -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. @@ -253,4 +257,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 diff --git a/packages/contracts/package.json b/packages/contracts/package.json index d6cc2c6a..be0397e2 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", @@ -51,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 066e3fd3..b35ca141 100644 --- a/packages/contracts/rollup.config.mjs +++ b/packages/contracts/rollup.config.mjs @@ -3,10 +3,12 @@ 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/facades', '@aedart/contracts/support/meta', '@aedart/contracts/support/mixins', '@aedart/contracts/support/reflections', 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 diff --git a/packages/contracts/src/container/Container.ts b/packages/contracts/src/container/Container.ts new file mode 100644 index 00000000..5c9625ea --- /dev/null +++ b/packages/contracts/src/container/Container.ts @@ -0,0 +1,328 @@ +import { + Callback, + Constructor, + ClassMethodReference +} from "@aedart/contracts"; +import { CallbackWrapper } from "@aedart/contracts/support"; +import { + Alias, + Identifier, + FactoryCallback, + ExtendCallback, + ReboundCallback, + BeforeResolvedCallback, + AfterResolvedCallback, +} from "./types"; +import Binding from "./Binding"; +import ContextualBindingBuilder from "./ContextualBindingBuilder"; + +/** + * 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 + */ +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(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[]} concrete + * + * @return {ContextualBindingBuilder} + * + * @throws {TypeError} + */ + when(...concrete: Constructor[]): ContextualBindingBuilder; + + /** + * 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; + + /** + * 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; + + /** + * Determine if identifier is an alias + * + * @param {Identifier} identifier + * + * @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 + * + * @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 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; + + /** + * Instantiate a new instance of given concrete + * + * @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, + args?: any[] /* eslint-disable-line @typescript-eslint/no-explicit-any */ + ): 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 */ + + /** + * Extend the registered binding + * + * @param {Identifier} identifier + * @param {ExtendCallback} callback + * + * @return {this} + * + * @throws {TypeError} + * @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 + * + * @param {Identifier} identifier + * + * @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 + * + * @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/ContextualBindingBuilder.ts b/packages/contracts/src/container/ContextualBindingBuilder.ts new file mode 100644 index 00000000..14781963 --- /dev/null +++ b/packages/contracts/src/container/ContextualBindingBuilder.ts @@ -0,0 +1,32 @@ +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} + * + * @throws {TypeError} + */ + give(implementation: FactoryCallback | Constructor): void; +} \ 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/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..dcc19dd3 --- /dev/null +++ b/packages/contracts/src/container/exceptions/index.ts @@ -0,0 +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 diff --git a/packages/contracts/src/container/index.ts b/packages/contracts/src/container/index.ts new file mode 100644 index 00000000..9e27f986 --- /dev/null +++ b/packages/contracts/src/container/index.ts @@ -0,0 +1,28 @@ +/** + * Container identifier + * + * @type {Symbol} + */ +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'); + +import Binding from "./Binding"; +import Container from "./Container"; +import ContextualBindingBuilder from "./ContextualBindingBuilder"; +export { + type Binding, + type Container, + type ContextualBindingBuilder, +} + +export * from './exceptions/index'; +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..de546f69 --- /dev/null +++ b/packages/contracts/src/container/types.ts @@ -0,0 +1,71 @@ +import { Callback, ConstructorLike } from "@aedart/contracts"; +import Container from "./Container"; + +/** + * Binding Identifier + * + * A unique identifier used for associating "concrete" items or values in + * a service container. + */ +export type Identifier = string | symbol | number | NonNullable | ConstructorLike | Callback; + +/** + * Binding Alias + * + * @see Identifier + */ +export type Alias = Identifier; + +/** + * Factory Callback + * + * The callback is responsible for resolving a value, when a binding + * is resolved in a service container. + */ +export type FactoryCallback< + Value = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +> = (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; + +/** + * Rebound Callback + * + * Callback to be invoked when a binding is "rebound". + */ +export type ReboundCallback< + Value = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ +> = (resolved: Value, container: Container) => void; + +/** + * Before Resolved Callback + * + * Callback to be invoked before a binding is resolved. + */ +export type BeforeResolvedCallback = ( + identifier: Identifier, + args: any[], /* eslint-disable-line @typescript-eslint/no-explicit-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 diff --git a/packages/contracts/src/support/CallbackWrapper.ts b/packages/contracts/src/support/CallbackWrapper.ts new file mode 100644 index 00000000..700d50ac --- /dev/null +++ b/packages/contracts/src/support/CallbackWrapper.ts @@ -0,0 +1,77 @@ +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; + + /** + * Determine if a binding has been set + * + * @return {boolean} + */ + hasBinding(): boolean; + + /** + * Invoke the callback + * + * @return {any} + * + * @throws {Error} + */ + call(): any; /* eslint-disable-line @typescript-eslint/no-explicit-any */ +} \ No newline at end of file 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/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/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/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 diff --git a/packages/contracts/src/support/facades/types.ts b/packages/contracts/src/support/facades/types.ts new file mode 100644 index 00000000..5d10d9cd --- /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 /* eslint-disable-line @typescript-eslint/no-explicit-any */ +> = (container: Container, identifier: Identifier) => T; \ No newline at end of file diff --git a/packages/contracts/src/support/index.ts b/packages/contracts/src/support/index.ts index c78f78ef..157c9b9f 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,13 @@ import Arrayable from "./Arrayable"; */ 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 Arrayable, + type CallbackWrapper, + type HasArbitraryData } export type * from './types'; \ No newline at end of file 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 6bf4f05b..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 @@ -57,6 +54,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/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/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 */ diff --git a/packages/contracts/src/types.ts b/packages/contracts/src/types.ts index b19e0e14..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 */ @@ -18,6 +23,33 @@ 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; \ No newline at end of file +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 + */ +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 diff --git a/packages/support/NOTICE b/packages/support/NOTICE index a57656f4..42a8dec5 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 @@ -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 diff --git a/packages/support/package.json b/packages/support/package.json index 13e02748..22254688 100644 --- a/packages/support/package.json +++ b/packages/support/package.json @@ -38,11 +38,21 @@ "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", "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", @@ -76,7 +86,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", diff --git a/packages/support/rollup.config.mjs b/packages/support/rollup.config.mjs index 299cbe5b..8b8428e8 100644 --- a/packages/support/rollup.config.mjs +++ b/packages/support/rollup.config.mjs @@ -4,10 +4,12 @@ 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', '@aedart/contracts/support/exceptions', + '@aedart/contracts/support/facades', '@aedart/contracts/support/meta', '@aedart/contracts/support/mixins', '@aedart/contracts/support/objects', diff --git a/packages/support/src/ArbitraryData.ts b/packages/support/src/ArbitraryData.ts new file mode 100644 index 00000000..6e472a86 --- /dev/null +++ b/packages/support/src/ArbitraryData.ts @@ -0,0 +1,99 @@ +import type { 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 + * @extends AbstractConcern + */ +export default class ArbitraryData extends AbstractConcern implements HasArbitraryData +{ + /** + * The arbitrary data record + * + * @type {Record} + * + * @protected + */ + protected _data: Record = {}; /* eslint-disable-line @typescript-eslint/no-explicit-any */ + + /** + * 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/CallbackWrapper.ts b/packages/support/src/CallbackWrapper.ts new file mode 100644 index 00000000..942c15a6 --- /dev/null +++ b/packages/support/src/CallbackWrapper.ts @@ -0,0 +1,288 @@ +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 + * + * @type {Callback} + * + * @protected + * @readonly + */ + protected readonly _callback: Callback; + + /** + * "This" value that callback is bound to + * + * @type {object | undefined} + * + * @readonly + * @protected + */ + protected _binding: object | undefined = undefined; + + /** + * Arguments to be passed on to the callback + * when invoked. + * + * @type {any[]} + * + * @protected + */ + protected _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('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); + } + + /** + * 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 + * + * @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/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..0736f49a 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/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 d0dea0ab..9c2b01e0 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 } from "@aedart/contracts/support/arrays"; +import Merger from "./merge/Merger"; /** - * Merge two or more arrays + * Returns new Array Merger instance * + * @return {ArrayMerger} + */ +export function merge(): ArrayMerger; + +/** + * 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 Merger(); + + if (sources.length == 0) { + return merger as ArrayMerger; } + + return merger.of(...sources); } \ 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/Merger.ts b/packages/support/src/arrays/merge/Merger.ts new file mode 100644 index 00000000..067b07fc --- /dev/null +++ b/packages/support/src/arrays/merge/Merger.ts @@ -0,0 +1,109 @@ +import type { + ArrayMerger, + 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 Merger implements ArrayMerger +{ + /** + * Merge options to be applied + * + * @type {Readonly} + * + * @protected + */ + protected _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/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..d1ada0bd --- /dev/null +++ b/packages/support/src/arrays/merge/index.ts @@ -0,0 +1,8 @@ +import Merger from './Merger'; +import DefaultArrayMergeOptions from './DefaultArrayMergeOptions'; +export { + Merger, + DefaultArrayMergeOptions +} + +export * from './defaultArrayMergeCallback'; \ No newline at end of file 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 6d72ca7b..070cfef7 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, @@ -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; } /** @@ -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..844841f9 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 + * @protected */ - #store: WeakMap< - ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor, + protected _store: WeakMap< + ConstructorLike | UsesConcerns | ConcernConstructor, Record >; @@ -25,31 +25,31 @@ export default class Repository implements DescriptorsRepository * Create new Descriptors instance */ constructor() { - this.#store = new WeakMap(); + this._store = new WeakMap(); } /** * Returns property descriptors for given target class (recursively) * - * @param {ConstructorOrAbstractConstructor | UsesConcerns | ConcernConstructor} target The target class, or concern class + * @param {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 { - 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; @@ -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,13 +95,13 @@ 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); + 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/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..cd4da577 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"; @@ -15,49 +15,49 @@ 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} * - * @type {ConstructorOrAbstractConstructor | UsesConcerns} + * @readonly + * @protected */ - readonly #source: ConstructorOrAbstractConstructor | UsesConcerns; + protected 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) @@ -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; } /** @@ -106,10 +106,10 @@ 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; + 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 28d465e8..c74e897b 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"; @@ -19,17 +19,26 @@ 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} * - * @type {ConstructorOrAbstractConstructor|UsesConcerns} + * @readonly + * @protected */ - readonly #source: ConstructorOrAbstractConstructor | 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: ConstructorOrAbstractConstructor | UsesConcerns, + target: ConstructorLike | UsesConcerns, concern: ConcernConstructor, - source: ConstructorOrAbstractConstructor | UsesConcerns, + source: ConstructorLike | UsesConcerns, message?: string, options?: ErrorOptions ) { @@ -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; @@ -53,10 +62,10 @@ 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; + 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 e5c31436..92553c30 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"; @@ -13,22 +13,23 @@ export default class InjectionError extends ConcernError implements InjectionExc /** * The target class * + * @type {ConstructorLike|UsesConcerns} + * + * @protected * @readonly - * - * @type {ConstructorOrAbstractConstructor|UsesConcerns} */ - readonly #target: ConstructorOrAbstractConstructor | UsesConcerns; + protected 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 @@ -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; @@ -48,10 +49,10 @@ 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; + 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 5d30e3bd..bbed4648 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"; @@ -15,25 +15,27 @@ 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 * - * @param {ConstructorOrAbstractConstructor | UsesConcerns} target + * @param {ConstructorLike | UsesConcerns} target * @param {ConcernConstructor} concern * @param {PropertyKey} alias * @param {PropertyKey} key @@ -41,7 +43,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, @@ -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/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); diff --git a/packages/support/src/container/dependencies.ts b/packages/support/src/container/dependencies.ts new file mode 100644 index 00000000..db40d807 --- /dev/null +++ b/packages/support/src/container/dependencies.ts @@ -0,0 +1,27 @@ +import type { Identifier } from "@aedart/contracts/container"; +import { DEPENDENCIES } from "@aedart/contracts/container"; +import { targetMeta } from "@aedart/support/meta"; + +/** + * 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 new file mode 100644 index 00000000..33ff2276 --- /dev/null +++ b/packages/support/src/container/index.ts @@ -0,0 +1,5 @@ +export * from "./dependencies"; +export * from "./dependsOn"; +export * from "./getDependencies"; +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/packages/support/src/facades/Container.ts b/packages/support/src/facades/Container.ts new file mode 100644 index 00000000..00349891 --- /dev/null +++ b/packages/support/src/facades/Container.ts @@ -0,0 +1,37 @@ +import type { Identifier, Container as ServiceContainer } from "@aedart/contracts/container"; +import { CONTAINER } from "@aedart/contracts/container"; +import Facade from "./Facade"; + +/** + * Container Facade + */ +export default class Container extends Facade +{ + /** + * 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 {import('@aedart/contracts/container').Container} + * + * @protected + * @static + */ + 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 CONTAINER; + } +} \ No newline at end of file diff --git a/packages/support/src/facades/Facade.ts b/packages/support/src/facades/Facade.ts new file mode 100644 index 00000000..0fd9da54 --- /dev/null +++ b/packages/support/src/facades/Facade.ts @@ -0,0 +1,345 @@ +import type { Container, Identifier } from "@aedart/contracts/container"; +import type { SpyFactoryCallback } from "@aedart/contracts/support/facades"; +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} + * + * @protected + * @static + */ + 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 + * + * @type {Map} + * + * @protected + * @static + */ + protected static resolved: Map< + Identifier, + any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + > = new Map(); + + /** + * Registered object spies + * + * @type {Set} + * + * @protected + * @static + */ + protected static spies: Set = new Set(); + + /** + * Facade Constructor + * + * @protected + */ + protected constructor() { + /* @ts-expect-error TS2345 Facade constructor is abstract */ + throw new AbstractClassError(this.constructor); + } + + /** + * Returns identifier to be used for resolving facade's underlying object instance + * + * @return {Identifier} + * + * @abstract + * + * @static + */ + public static getIdentifier(): Identifier + { + throw new LogicalError('Facade does not implement the getIdentifier() method'); + } + + /** + * Obtain the underlying object instance, or a "spy" (for testing) + * + * @see spy + * + * @return {any} + * + * @throws {NotFoundException} + * @throws {ContainerException} + * @throws {LogicalError} + * + * @abstract + * + * @static + */ + public static obtain() + { + return this.resolve(this.getIdentifier()); + } + + /** + * Register a "spy" (e.g. object mock) for this facade's identifier + * + * @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 {T} Object instance (spy / object mock) to be registered in service container. + * + * @static + */ + public static spy< + T = any /* eslint-disable-line @typescript-eslint/no-explicit-any */ + >(callback: SpyFactoryCallback): T + { + const identifier = this.getIdentifier(); + const spy = callback(this.getContainer() as Container, 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 /* eslint-disable-line @typescript-eslint/no-explicit-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): typeof 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(): typeof 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(); + } + + /** + * Clears all resolved instances, service container and evt. spies. + * + * @return {void} + */ + public static destroy(): void + { + this.forgetAllSpies(); + this.forgetAllResolved(); + this.forgetContainer(); + } + + /** + * 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 + * + * @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 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 diff --git a/packages/support/src/index.ts b/packages/support/src/index.ts index 2486adb2..d139c7cb 100644 --- a/packages/support/src/index.ts +++ b/packages/support/src/index.ts @@ -3,4 +3,13 @@ * * @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 ArbitraryData from "./ArbitraryData"; +import CallbackWrapper from "./CallbackWrapper"; +export { + ArbitraryData, + CallbackWrapper +} + +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/packages/support/src/meta/MetaRepository.ts b/packages/support/src/meta/MetaRepository.ts index 90cc0b85..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; } /** @@ -140,17 +141,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); } @@ -248,10 +246,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 diff --git a/packages/support/src/meta/target/TargetMetaRepository.ts b/packages/support/src/meta/target/TargetMetaRepository.ts index 13d22e2d..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. @@ -139,6 +136,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/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() {} diff --git a/packages/support/src/mixins/Builder.ts b/packages/support/src/mixins/Builder.ts index 744fc76d..d573375d 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,18 +18,21 @@ export default class Builder /** * The target superclass * - * @type {ConstructorOrAbstractConstructor} - * @private + * @template T = object + * + * @type {ConstructorLike} + * + * @protected */ - readonly #superclass: ConstructorOrAbstractConstructor; + protected readonly _superclass: ConstructorLike; /** * Create a new Mixin Builder instance * - * @param {ConstructorOrAbstractConstructor} [superclass=class {}] + * @param {ConstructorLike} [superclass=class {}] */ - constructor(superclass:ConstructorOrAbstractConstructor = class {} as ConstructorOrAbstractConstructor) { - this.#superclass = superclass; + constructor(superclass:ConstructorLike = class {} as ConstructorLike) { + this._superclass = superclass; } /** @@ -37,12 +40,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 +55,7 @@ export default class Builder // Apply the mixin... return mixin(superclass); - }, this.#superclass as ConstructorOrAbstractConstructor) as ConstructorOrAbstractConstructor; + }, this._superclass as ConstructorLike) as ConstructorLike; } /** @@ -63,12 +66,12 @@ 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; + return this._superclass; } } \ No newline at end of file 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/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/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/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; } 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 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/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; } } 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 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 diff --git a/packages/support/src/reflections/isClassConstructor.ts b/packages/support/src/reflections/isClassConstructor.ts index f3a1dae1..06c31747 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 style) * * @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); 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/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/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 { 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 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 diff --git a/tests/browser/packages/container/container/alias.test.js b/tests/browser/packages/container/container/alias.test.js new file mode 100644 index 00000000..bfd89ee5 --- /dev/null +++ b/tests/browser/packages/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 diff --git a/tests/browser/packages/container/container/bind.test.js b/tests/browser/packages/container/container/bind.test.js new file mode 100644 index 00000000..eb238629 --- /dev/null +++ b/tests/browser/packages/container/container/bind.test.js @@ -0,0 +1,64 @@ +import { Container } from "@aedart/container"; + +describe('@aedart/support/container', () => { + describe('bind', () => { + + it('can bind factory callback', () => { + const container = new Container(); + + container.bind('alpha', () => 'beta'); + + // -------------------------------------------------------------------- // + + expect(container.bound('alpha')) + .withContext('Binding not registered in container') + .toBeTrue(); + + 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/container/container/build.test.js b/tests/browser/packages/container/container/build.test.js new file mode 100644 index 00000000..5fb55b77 --- /dev/null +++ b/tests/browser/packages/container/container/build.test.js @@ -0,0 +1,196 @@ +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; + } + } + + // -------------------------------------------------------------------- // + + 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 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 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 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 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 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 diff --git a/tests/browser/packages/container/container/getInstance.test.js b/tests/browser/packages/container/container/getInstance.test.js new file mode 100644 index 00000000..cf1b5685 --- /dev/null +++ b/tests/browser/packages/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 diff --git a/tests/browser/packages/container/container/instance.test.js b/tests/browser/packages/container/container/instance.test.js new file mode 100644 index 00000000..da960022 --- /dev/null +++ b/tests/browser/packages/container/container/instance.test.js @@ -0,0 +1,29 @@ +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(container.isShared('a')) + .withContext('Instance is not marked as "shared"') + .toBeTrue(); + + expect(result) + .toBe(instance); + }); + }); +}); \ No newline at end of file diff --git a/tests/browser/packages/container/container/make.test.js b/tests/browser/packages/container/container/make.test.js new file mode 100644 index 00000000..2c0df886 --- /dev/null +++ b/tests/browser/packages/container/container/make.test.js @@ -0,0 +1,135 @@ +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); + }); + }); + + 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 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 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 diff --git a/tests/browser/packages/container/container/singleton.test.js b/tests/browser/packages/container/container/singleton.test.js new file mode 100644 index 00000000..c250b4c0 --- /dev/null +++ b/tests/browser/packages/container/container/singleton.test.js @@ -0,0 +1,76 @@ +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()); + + // -------------------------------------------------------------------- // + + expect(container.isShared('a')) + .withContext('Binding is not marked as "shared"') + .toBeTrue(); + + 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 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 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 diff --git a/tests/browser/packages/support/CallbackWrapper.test.js b/tests/browser/packages/support/CallbackWrapper.test.js new file mode 100644 index 00000000..d233ab8c --- /dev/null +++ b/tests/browser/packages/support/CallbackWrapper.test.js @@ -0,0 +1,215 @@ +import { CallbackWrapper, ArbitraryData } from "@aedart/support"; +import { usesConcerns } from "@aedart/support/concerns"; + +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}`); + }); + + 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); + }); + + 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 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 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 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 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..43f8e238 --- /dev/null +++ b/tests/browser/packages/support/facades/Facade.test.js @@ -0,0 +1,197 @@ +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('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'; + } + } + + 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 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 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); diff --git a/tests/browser/packages/support/reflections/isCallable-isConstructor.test.js b/tests/browser/packages/support/reflections/isCallable-isConstructor.test.js deleted file mode 100644 index 6006a5bb..00000000 --- a/tests/browser/packages/support/reflections/isCallable-isConstructor.test.js +++ /dev/null @@ -1,103 +0,0 @@ -import { - isCallable, - isClassConstructor, - isConstructor -} from "@aedart/support/reflections"; - -describe('@aedart/support/reflections', () => { - - describe('isConstructor', () => { - - it('can determine if is constructor', () => { - - const classWithConstructor = class { - constructor() { - throw new TypeError('Actual constructor invoked in class!'); - } - } - - const classWithStaticMethod = class { - static foo() { - throw new TypeError('Static method is invoked in class!'); - } - } - - 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 }, - ]; - - data.forEach((entry, index) => { - - let result = isConstructor(entry.value); - expect(result) - .withContext(`Value at index ${index} was expected to be ${entry.expected}`) - .toBe(entry.expected); - }); - }); - - }); - - 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); - }); - }); - - }); - - 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/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/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 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 diff --git a/tests/browser/packages/support/reflections/isConstructor.test.js b/tests/browser/packages/support/reflections/isConstructor.test.js new file mode 100644 index 00000000..c7ebd846 --- /dev/null +++ b/tests/browser/packages/support/reflections/isConstructor.test.js @@ -0,0 +1,58 @@ +import { + isConstructor +} from "@aedart/support/reflections"; + +describe('@aedart/support/reflections', () => { + + describe('isConstructor()', () => { + + it('can determine if is constructor', () => { + + const classWithConstructor = class { + constructor() { + throw new TypeError('Actual constructor invoked in class!'); + } + } + + const classWithStaticMethod = class { + static foo() { + throw new TypeError('Static method is invoked in class!'); + } + } + + const data = [ + { 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(`${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/isMethod.test.js b/tests/browser/packages/support/reflections/isMethod.test.js new file mode 100644 index 00000000..468c4361 --- /dev/null +++ b/tests/browser/packages/support/reflections/isMethod.test.js @@ -0,0 +1,49 @@ +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: [ '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' }, + { 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 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" },