diff --git a/src/index.ts b/src/index.ts index afaec31d5..af97cf074 100644 --- a/src/index.ts +++ b/src/index.ts @@ -423,6 +423,7 @@ export * from './util/errors/UnauthorizedHttpError'; export * from './util/errors/UnsupportedMediaTypeHttpError'; // Util/Handlers +export * from './util/handlers/ArrayUnionHandler'; export * from './util/handlers/AsyncHandler'; export * from './util/handlers/BooleanHandler'; export * from './util/handlers/ConditionalHandler'; diff --git a/src/util/handlers/ArrayUnionHandler.ts b/src/util/handlers/ArrayUnionHandler.ts new file mode 100644 index 000000000..243b9b377 --- /dev/null +++ b/src/util/handlers/ArrayUnionHandler.ts @@ -0,0 +1,15 @@ +import type { AsyncHandler, AsyncHandlerOutput } from './AsyncHandler'; +import { UnionHandler } from './UnionHandler'; + +/** + * A utility handler that concatenates the results of all its handlers into a single result. + */ +export class ArrayUnionHandler> extends UnionHandler { + public constructor(handlers: T[], requireAll?: boolean, ignoreErrors?: boolean) { + super(handlers, requireAll, ignoreErrors); + } + + protected async combine(results: AsyncHandlerOutput[]): Promise> { + return results.flat() as AsyncHandlerOutput; + } +} diff --git a/src/util/handlers/AsyncHandler.ts b/src/util/handlers/AsyncHandler.ts index 52e42a572..dd5ca8c80 100644 --- a/src/util/handlers/AsyncHandler.ts +++ b/src/util/handlers/AsyncHandler.ts @@ -1,3 +1,7 @@ +type Awaited = T extends PromiseLike ? U : T; +export type AsyncHandlerInput> = Parameters[0]; +export type AsyncHandlerOutput> = Awaited>; + /** * Simple interface for classes that can potentially handle a specific kind of data asynchronously. */ diff --git a/src/util/handlers/UnionHandler.ts b/src/util/handlers/UnionHandler.ts index 2a63656ca..8ed7c3cbb 100644 --- a/src/util/handlers/UnionHandler.ts +++ b/src/util/handlers/UnionHandler.ts @@ -1,18 +1,15 @@ import { allFulfilled } from '../PromiseUtil'; +import type { AsyncHandlerInput, AsyncHandlerOutput } from './AsyncHandler'; import { AsyncHandler } from './AsyncHandler'; import { filterHandlers, findHandler } from './HandlerUtil'; -// Helper types to make sure the UnionHandler has the same in/out types as the AsyncHandler type it wraps -type Awaited = T extends PromiseLike ? U : T; -type InType> = Parameters[0]; -type OutType> = Awaited>; - /** * Utility handler that allows combining the results of multiple handlers into one. * Will run the handlers and then call the abstract `combine` function with the results, * which then generates the handler's output. */ -export abstract class UnionHandler> extends AsyncHandler, OutType> { +export abstract class UnionHandler> extends + AsyncHandler, AsyncHandlerOutput> { protected readonly handlers: T[]; private readonly requireAll: boolean; private readonly ignoreErrors: boolean; @@ -38,7 +35,7 @@ export abstract class UnionHandler> extends Asy this.ignoreErrors = ignoreErrors; } - public async canHandle(input: InType): Promise { + public async canHandle(input: AsyncHandlerInput): Promise { if (this.requireAll) { await this.allCanHandle(input); } else { @@ -47,9 +44,9 @@ export abstract class UnionHandler> extends Asy } } - public async handle(input: InType): Promise> { + public async handle(input: AsyncHandlerInput): Promise> { const handlers = this.requireAll ? this.handlers : await filterHandlers(this.handlers, input); - const results = handlers.map((handler): Promise> => handler.handle(input)); + const results = handlers.map((handler): Promise> => handler.handle(input)); return this.combine(await allFulfilled(results, this.ignoreErrors)); } @@ -57,12 +54,12 @@ export abstract class UnionHandler> extends Asy * Checks if all handlers can handle the input. * If not, throw an error based on the errors of the failed handlers. */ - protected async allCanHandle(input: InType): Promise { + protected async allCanHandle(input: AsyncHandlerInput): Promise { await allFulfilled(this.handlers.map((handler): Promise => handler.canHandle(input))); } /** * Combines the results of the handlers into a single output. */ - protected abstract combine(results: OutType[]): Promise>; + protected abstract combine(results: AsyncHandlerOutput[]): Promise>; } diff --git a/test/unit/util/handlers/ArrayUnionHandler.test.ts b/test/unit/util/handlers/ArrayUnionHandler.test.ts new file mode 100644 index 000000000..1f89f4ef3 --- /dev/null +++ b/test/unit/util/handlers/ArrayUnionHandler.test.ts @@ -0,0 +1,30 @@ +import { ArrayUnionHandler } from '../../../../src/util/handlers/ArrayUnionHandler'; +import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler'; + +describe('An ArrayUnionHandler', (): void => { + let handlers: jest.Mocked>[]; + let handler: ArrayUnionHandler>; + + beforeEach(async(): Promise => { + handlers = [ + { + canHandle: jest.fn(), + handle: jest.fn().mockResolvedValue([ 1, 2 ]), + } as any, + { + canHandle: jest.fn(), + handle: jest.fn().mockResolvedValue([ 3, 4 ]), + } as any, + ]; + + handler = new ArrayUnionHandler(handlers); + }); + + it('merges the array results.', async(): Promise => { + await expect(handler.handle('input')).resolves.toEqual([ 1, 2, 3, 4 ]); + expect(handlers[0].handle).toHaveBeenCalledTimes(1); + expect(handlers[0].handle).toHaveBeenLastCalledWith('input'); + expect(handlers[1].handle).toHaveBeenCalledTimes(1); + expect(handlers[1].handle).toHaveBeenLastCalledWith('input'); + }); +});