diff --git a/__tests__/behavioral/Plugin.spec.ts b/__tests__/behavioral/Plugin.spec.ts new file mode 100644 index 0000000..179f93a --- /dev/null +++ b/__tests__/behavioral/Plugin.spec.ts @@ -0,0 +1,118 @@ +/** + * BSD 3-Clause License + * + * Copyright © 2023, Daniel Jonathan + * 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. + */ + +import { + it, + expect, + describe, +} from 'vitest' + +import { + Plugin, + PluginManager, +} from '@/index' + +type Data = { + prop: number +} + +class PluginA extends Plugin { + get name(): string { + return 'Plugin A' + } + + execute(data: Data): void { + ++data.prop + } +} + +class PluginB extends Plugin { + get name(): string { + return 'Plugin B' + } + + execute(data: Data): void { + data.prop += 2 + } +} + +describe('Plugin', () => { + it('plugin execution', () => { + const pm = new PluginManager() + + const a = new PluginA() + const b = new PluginB() + + const data = { + prop: 0, + } + + expect(pm.register(a)).toBeTruthy() + expect(pm.register(a)).toBeFalsy() + + expect(pm.register(b)).toBeTruthy() + expect(pm.register(b)).toBeFalsy() + + expect(pm.deregister('bogus plugin')).toBeFalsy() + + pm.execute(data) + + expect(data.prop).toBe(3) + }) + + it('plugin removal', () => { + const pm = new PluginManager() + + const a = new PluginA() + const b = new PluginB() + + const data = { + prop: 0, + } + + expect(pm.register(a)).toBeTruthy() + expect(pm.register(a)).toBeFalsy() + + expect(pm.register(b)).toBeTruthy() + expect(pm.register(b)).toBeFalsy() + + expect(pm.deregister(a)).toBeTruthy() + expect(pm.deregister(a.name)).toBeFalsy() + + pm.execute(data) + + expect(data.prop).toBe(2) + + expect(pm.deregister(b.name)).toBeTruthy() + expect(pm.deregister(a)).toBeFalsy() + }) +}) \ No newline at end of file diff --git a/__tests__/behavioral/RequestChain.spec.ts b/__tests__/behavioral/ProcessChain.spec.ts similarity index 85% rename from __tests__/behavioral/RequestChain.spec.ts rename to __tests__/behavioral/ProcessChain.spec.ts index 46a7026..e33acc0 100644 --- a/__tests__/behavioral/RequestChain.spec.ts +++ b/__tests__/behavioral/ProcessChain.spec.ts @@ -44,30 +44,30 @@ type Data = { prop: number } -class DataProcessChain extends ProcessChain { +class ProcessableChain extends ProcessChain { isProcessable(data: Data): boolean { return 'prop' in data } - protected execute(data: Data): void { + protected processor(data: Data): void { ++data.prop } } -class UnprocessableProcessChain extends ProcessChain { +class UnprocessableChain extends ProcessChain { isProcessable(data: Data): boolean { return !('prop' in data) } - protected execute(data: Data): void { + protected processor(data: Data): void { ++data.prop } } describe('ProcessChain', () => { it('count the links in the chain', () => { - const a = new DataProcessChain() - const b = new DataProcessChain() + const a = new ProcessableChain() + const b = new ProcessableChain() const data = { prop: 0, @@ -77,15 +77,15 @@ describe('ProcessChain', () => { expect(b.isProcessable(data)).toBeTruthy() a.append(b) - a.process(data) + a.execute(data) expect(data.prop).toBe(1) }) it('break the links in the chain', () => { - const a = new UnprocessableProcessChain() - const b = new DataProcessChain() - const c = new DataProcessChain() + const a = new UnprocessableChain() + const b = new ProcessableChain() + const c = new ProcessableChain() const data = { prop: 0, @@ -97,7 +97,7 @@ describe('ProcessChain', () => { a.append(b) b.append(c) - a.process(data) + a.execute(data) expect(data.prop).toBe(1) }) diff --git a/src/behavioral/Plugin.ts b/src/behavioral/Plugin.ts new file mode 100644 index 0000000..1c54971 --- /dev/null +++ b/src/behavioral/Plugin.ts @@ -0,0 +1,125 @@ +/** + * BSD 3-Clause License + * + * Copyright © 2023, Daniel Jonathan + * 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. + */ + +/** + * @module Plugin + */ + +export abstract class Plugin { + /** + * Retrieves the name. + * + * @returns {string} The name. + */ + abstract get name(): string + + /** + * Executes the function with the given arguments. + * + * @param {...T} args - The arguments to be passed to the function. + * @return {void} + */ + abstract execute(...args: T[]): void +} + +export class PluginManager { + protected plugins: Plugin[] + + constructor() { + this.plugins = [] + } + + /** + * Registers a plugin. + * + * @param {Plugin} plugin - The plugin to register. + * @return {boolean} - Returns true if the plugin is registered successfully, otherwise returns false. + */ + register(plugin: Plugin): boolean { + const i = this.indexOf(plugin) + + if (-1 === i) { + this.plugins.push(plugin) + return true + } + + return false + } + + /** + * Deregisters a plugin from the system. + * + * @param {Plugin | string} plugin - The plugin or the name of the plugin to be deregistered. + * @return {boolean} - Returns true if the plugin was successfully deregistered, otherwise false. + */ + deregister(plugin: Plugin | string): boolean { + const i = this.indexOf(plugin) + + if (-1 < i) { + this.plugins.splice(i, 1) + return true + } + + return false + } + + /** + * Executes the `execute` method of all plugins. + * + * @param {...T} args - The arguments to pass to the `execute` method. + * @return {void} + */ + execute(...args: T[]): void { + for (const p of this.plugins) { + p.execute(...args) + } + } + + /** + * Finds the index of the specified plugin in the plugins array. + * + * @param {Plugin | string} plugin - The plugin to search for. Can be either a Plugin object or a string representing the plugin name. + * @return {number} - The index of the plugin in the plugins array. Returns -1 if the plugin is not found. + */ + protected indexOf(plugin: Plugin | string): number { + const plugins = this.plugins + const name = 'string' === typeof plugin ? plugin : plugin.name + + for (let i = plugins.length - 1; i >= 0; --i) { + if (name === plugins[i].name) { + return i + } + } + + return -1 + } +} \ No newline at end of file diff --git a/src/behavioral/ProcessChain.ts b/src/behavioral/ProcessChain.ts index f546d7a..d8d23ee 100644 --- a/src/behavioral/ProcessChain.ts +++ b/src/behavioral/ProcessChain.ts @@ -47,20 +47,20 @@ export type Chainable = { get next(): Nullable> /** - * Executes the provided argument of type T. + * Executes the method with the given arguments. * - * @param {T} arg - The argument to be executed. - * @return {void} - This method does not return any value. + * @param {...T} args - The arguments to be passed to the method. + * @return {void} */ - process(arg: T): void + execute(...args: T[]): void /** - * Checks if the provided argument is processable. + * Checks if the given arguments can be processed. * - * @param {T} arg - The argument to be checked for processability. - * @return {boolean} - Returns `true` if the argument is processable, otherwise returns `false`. + * @param {...T} args - The arguments to be checked. + * @return {void} */ - isProcessable(arg: T): boolean + isProcessable(...args: T[]): void } export abstract class ProcessChain implements Chainable { @@ -99,34 +99,37 @@ export abstract class ProcessChain implements Chainable { } /** - * Executes the method with the given argument if the handle function returns false. - * If the handle function returns true, the method is not executed and the next method in the chain is called recursively. + * Executes the processor if the given arguments are executable, + * otherwise passes the arguments to the next execute method in the chain. * - * @param {T} arg - The argument to be passed to the method. - * @return {void} + * @template T - Type of the arguments. + * + * @param {...T[]} args - The arguments to be passed to the processor. + * + * @returns {void} */ - process(arg: T): void { - if (this.isProcessable(arg)) { - this.execute(arg) + execute(...args: T[]): void { + if (this.isProcessable(...args)) { + this.processor(...args) } else { - this.next?.process(arg) + this.next?.execute(...args) } } /** - * Checks if the given argument can be processed. + * Determines if the given arguments are processable. * - * @param {T} arg - The argument to check if it is processable. - * @return {boolean} - `true` if the argument is processable, otherwise `false`. + * @param {...T} args - The arguments to be checked for processability. + * @return {boolean} - True if the arguments are processable, false otherwise. */ - abstract isProcessable(arg: T): boolean + abstract isProcessable(...args: T[]): boolean /** - * Executes the given argument. + * Process the given arguments of type T. * - * @param {T} arg - The argument to be executed. + * @param {...T} args - The arguments to be processed. * @return {void} */ - protected abstract execute(arg: T): void + protected abstract processor(...args: T[]): void } diff --git a/src/behavioral/index.ts b/src/behavioral/index.ts index 27652ea..8c3789b 100644 --- a/src/behavioral/index.ts +++ b/src/behavioral/index.ts @@ -31,4 +31,5 @@ */ export * from '@/behavioral/Observable' +export * from '@/behavioral/Plugin' export * from '@/behavioral/ProcessChain' \ No newline at end of file