Skip to content

Commit

Permalink
added Plugin pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel Jonathan committed Jul 14, 2024
1 parent 7f55219 commit ea2ecdb
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 34 deletions.
118 changes: 118 additions & 0 deletions __tests__/behavioral/Plugin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* BSD 3-Clause License
*
* Copyright © 2023, Daniel Jonathan <daniel at cosmicmind dot com>
* 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<Data> {
get name(): string {
return 'Plugin A'
}

execute(data: Data): void {
++data.prop
}
}

class PluginB extends Plugin<Data> {
get name(): string {
return 'Plugin B'
}

execute(data: Data): void {
data.prop += 2
}
}

describe('Plugin', () => {
it('plugin execution', () => {
const pm = new PluginManager<Data>()

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<Data>()

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()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -44,30 +44,30 @@ type Data = {
prop: number
}

class DataProcessChain extends ProcessChain<Data> {
class ProcessableChain extends ProcessChain<Data> {
isProcessable(data: Data): boolean {
return 'prop' in data
}

protected execute(data: Data): void {
protected processor(data: Data): void {
++data.prop
}
}

class UnprocessableProcessChain extends ProcessChain<Data> {
class UnprocessableChain extends ProcessChain<Data> {
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,
Expand All @@ -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,
Expand All @@ -97,7 +97,7 @@ describe('ProcessChain', () => {

a.append(b)
b.append(c)
a.process(data)
a.execute(data)

expect(data.prop).toBe(1)
})
Expand Down
125 changes: 125 additions & 0 deletions src/behavioral/Plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* BSD 3-Clause License
*
* Copyright © 2023, Daniel Jonathan <daniel at cosmicmind dot com>
* 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<T> {
/**
* 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<T> {
protected plugins: Plugin<T>[]

constructor() {
this.plugins = []
}

/**
* Registers a plugin.
*
* @param {Plugin<T>} plugin - The plugin to register.
* @return {boolean} - Returns true if the plugin is registered successfully, otherwise returns false.
*/
register(plugin: Plugin<T>): 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<T> | 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<T> | 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<T> | 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<T> | 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
}
}
49 changes: 26 additions & 23 deletions src/behavioral/ProcessChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,20 @@ export type Chainable<T> = {
get next(): Nullable<Chainable<T>>

/**
* 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<T> implements Chainable<T> {
Expand Down Expand Up @@ -99,34 +99,37 @@ export abstract class ProcessChain<T> implements Chainable<T> {
}

/**
* 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
}
Loading

0 comments on commit ea2ecdb

Please sign in to comment.