-
Notifications
You must be signed in to change notification settings - Fork 124
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Create UnionHandler to combine AsyncHandler results
- Loading branch information
Showing
3 changed files
with
159 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { AsyncHandler } from './AsyncHandler'; | ||
import { createAggregateError, filterHandlers, findHandler } from './HandlerUtil'; | ||
|
||
// Helper types to make sure the UnionHandler has the same in/out types as the AsyncHandler type it wraps | ||
type ThenArg<T> = T extends PromiseLike<infer U> ? U : T; | ||
type InType<T extends AsyncHandler<any, any>> = Parameters<T['handle']>[0]; | ||
type OutType<T extends AsyncHandler<any, any>> = ThenArg<ReturnType<T['handle']>>; | ||
type HandlerType<T extends AsyncHandler> = AsyncHandler<InType<T>, OutType<T>>; | ||
|
||
/** | ||
* Utility handler that allows combining the results of multiple handlers into one. | ||
* Will run all the handlers and then call the abstract `combine` function with the results, | ||
* which should return the output of the class. | ||
* | ||
* If `requireAll` is true, the handler will fail if any of the handlers do not support the input. | ||
* If `requireAll` is false, only the handlers that support the input will be called, | ||
* only if all handlers reject the input will this handler reject as well. | ||
* With `requireAll` set to false, the length of the input array | ||
* for the `combine` function is variable (but always at least 1). | ||
*/ | ||
export abstract class UnionHandler<T extends AsyncHandler<any, any>> extends AsyncHandler<InType<T>, OutType<T>> { | ||
protected readonly handlers: T[]; | ||
private readonly requireAll: boolean; | ||
|
||
protected constructor(handlers: T[], requireAll = false) { | ||
super(); | ||
this.handlers = handlers; | ||
this.requireAll = requireAll; | ||
} | ||
|
||
public async canHandle(input: InType<T>): Promise<void> { | ||
if (this.requireAll) { | ||
await this.allCanHandle(input); | ||
} else { | ||
// This will error if no handler supports the input | ||
await findHandler(this.handlers, input); | ||
} | ||
} | ||
|
||
public async handle(input: InType<T>): Promise<OutType<T>> { | ||
let handlers: HandlerType<T>[]; | ||
if (this.requireAll) { | ||
// Handlers were already checked in canHandle | ||
// eslint-disable-next-line prefer-destructuring | ||
handlers = this.handlers; | ||
} else { | ||
handlers = await filterHandlers(this.handlers, input); | ||
} | ||
|
||
const results = await Promise.all( | ||
handlers.map(async(handler): Promise<OutType<T>> => handler.handle(input)), | ||
); | ||
|
||
return this.combine(results); | ||
} | ||
|
||
public async handleSafe(input: InType<T>): Promise<OutType<T>> { | ||
let handlers: HandlerType<T>[]; | ||
if (this.requireAll) { | ||
await this.allCanHandle(input); | ||
// eslint-disable-next-line prefer-destructuring | ||
handlers = this.handlers; | ||
} else { | ||
// This will error if no handler supports the input | ||
handlers = await filterHandlers(this.handlers, input); | ||
} | ||
|
||
const results = await Promise.all( | ||
handlers.map(async(handler): Promise<OutType<T>> => handler.handle(input)), | ||
); | ||
|
||
return this.combine(results); | ||
} | ||
|
||
/** | ||
* Checks if all handlers can handle the input. | ||
* If not, throw an error based on the errors of the failed handlers. | ||
*/ | ||
private async allCanHandle(input: InType<T>): Promise<void> { | ||
const results = await Promise.allSettled(this.handlers.map(async(handler): Promise<HandlerType<T>> => { | ||
await handler.canHandle(input); | ||
return handler; | ||
})); | ||
if (results.some(({ status }): boolean => status === 'rejected')) { | ||
const errors = results.map((result): Error => (result as PromiseRejectedResult).reason); | ||
throw createAggregateError(errors); | ||
} | ||
} | ||
|
||
/** | ||
* Combine the results of the handlers into a single output. | ||
*/ | ||
protected abstract combine(results: OutType<T>[]): Promise<OutType<T>>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler'; | ||
import { UnionHandler } from '../../../../src/util/handlers/UnionHandler'; | ||
|
||
class SimpleUnionHandler extends UnionHandler<AsyncHandler<any, string>> { | ||
public constructor(handlers: AsyncHandler<any, any>[], requireAll?: boolean) { | ||
super(handlers, requireAll); | ||
} | ||
|
||
protected async combine(results: string[]): Promise<string> { | ||
return results.join(''); | ||
} | ||
} | ||
|
||
describe('A UnionHandler', (): void => { | ||
const input = { data: 'text' }; | ||
let handlers: jest.Mocked<AsyncHandler<any, string>>[]; | ||
let handler: SimpleUnionHandler; | ||
|
||
beforeEach(async(): Promise<void> => { | ||
handlers = [ | ||
{ canHandle: jest.fn(), handle: jest.fn().mockResolvedValue('a') } as any, | ||
{ canHandle: jest.fn(), handle: jest.fn().mockResolvedValue('b') } as any, | ||
]; | ||
|
||
handler = new SimpleUnionHandler(handlers); | ||
}); | ||
|
||
it('can handle a request if at least one extractor can handle it.', async(): Promise<void> => { | ||
await expect(handler.canHandle(input)).resolves.toBeUndefined(); | ||
|
||
handlers[0].canHandle.mockRejectedValue(new Error('bad request')); | ||
await expect(handler.canHandle(input)).resolves.toBeUndefined(); | ||
|
||
handlers[1].canHandle.mockRejectedValue(new Error('bad request')); | ||
await expect(handler.canHandle(input)).rejects.toThrow('bad request'); | ||
|
||
await expect(handler.handleSafe(input)).rejects.toThrow('bad request'); | ||
}); | ||
|
||
it('requires all handlers to support the input if requireAll is true.', async(): Promise<void> => { | ||
handler = new SimpleUnionHandler(handlers, true); | ||
await expect(handler.canHandle(input)).resolves.toBeUndefined(); | ||
|
||
handlers[0].canHandle.mockRejectedValue(new Error('bad request')); | ||
await expect(handler.canHandle(input)).rejects.toThrow('bad request'); | ||
|
||
await expect(handler.handleSafe(input)).rejects.toThrow('bad request'); | ||
}); | ||
|
||
it('calls all handlers that support the input.', async(): Promise<void> => { | ||
handlers[0].canHandle.mockRejectedValue(new Error('bad request')); | ||
await expect(handler.handle(input)).resolves.toBe('b'); | ||
await expect(handler.handleSafe(input)).resolves.toBe('b'); | ||
}); | ||
|
||
it('calls all handlers if requireAll is true.', async(): Promise<void> => { | ||
handler = new SimpleUnionHandler(handlers, true); | ||
await expect(handler.handleSafe(input)).resolves.toBe('ab'); | ||
|
||
// `handle` call does not need to check `canHandle` values anymore | ||
handlers[0].canHandle.mockRejectedValue(new Error('bad request')); | ||
await expect(handler.handle(input)).resolves.toBe('ab'); | ||
}); | ||
}); |