-
Notifications
You must be signed in to change notification settings - Fork 131
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add CompositeAsyncHandler to support multiple handlers
- Loading branch information
Showing
14 changed files
with
275 additions
and
23 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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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
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
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 |
---|---|---|
@@ -1,17 +1,35 @@ | ||
/** | ||
* Simple interface for classes that can potentially handle a specific kind of data asynchronously. | ||
*/ | ||
export interface AsyncHandler<TInput, TOutput = void> { | ||
export abstract class AsyncHandler<TInput, TOutput = void> { | ||
/** | ||
* Checks if the input data can be handled by this class. | ||
* Throws an error if it can't handle the data. | ||
* @param input - Input data that would be handled potentially. | ||
* @returns A promise resolving to if this input can be handled. | ||
* | ||
* @returns A promise resolving if this input can be handled, rejecting with an Error if not. | ||
*/ | ||
canHandle: (input: TInput) => Promise<boolean>; | ||
public abstract canHandle (input: TInput): Promise<void>; | ||
|
||
/** | ||
* Handles the given input. This should only be done if the {@link canHandle} function returned `true`. | ||
* @param input - Input data that needs to be handled. | ||
* | ||
* @returns A promise resolving when the handling is finished. Return value depends on the given type. | ||
*/ | ||
handle: (input: TInput) => Promise<TOutput>; | ||
public abstract handle (input: TInput): Promise<TOutput>; | ||
|
||
/** | ||
* Helper function that first runs the canHandle function followed by the handle function. | ||
* Throws the error of the {@link canHandle} function if the data can't be handled, | ||
* or returns the result of the {@link handle} function otherwise. | ||
* @param data - The data to handle. | ||
* | ||
* @returns The result of the handle function of the handler. | ||
*/ | ||
public async handleSafe (data: TInput): Promise<TOutput> { | ||
await this.canHandle(data); | ||
|
||
return this.handle(data); | ||
} | ||
} |
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,86 @@ | ||
import { AsyncHandler } from './AsyncHandler'; | ||
import { UnsupportedHttpError } from './errors/UnsupportedHttpError'; | ||
|
||
/** | ||
* Handler that combines several other handlers, | ||
* thereby allowing other classes that depend on a single handler to still use multiple. | ||
*/ | ||
export class CompositeAsyncHandler<TIn, TOut> implements AsyncHandler<TIn, TOut> { | ||
private readonly handlers: AsyncHandler<TIn, TOut>[]; | ||
|
||
/** | ||
* Creates a new CompositeAsyncHandler that stores the given handlers. | ||
* @param handlers - Handlers over which it will run. | ||
*/ | ||
public constructor (handlers: AsyncHandler<TIn, TOut>[]) { | ||
this.handlers = handlers; | ||
} | ||
|
||
/** | ||
* Checks if any of the stored handlers can handle the given input. | ||
* @param input - The data that would need to be handled. | ||
* | ||
* @returns A promise resolving if at least 1 handler supports to input, or rejecting if none do. | ||
*/ | ||
public async canHandle (input: TIn): Promise<void> { | ||
await this.findHandler(input); | ||
} | ||
|
||
/** | ||
* Finds a handler that supports the given input and then lets it handle the given data. | ||
* @param input - The data that needs to be handled. | ||
* | ||
* @returns A promise corresponding to the handle call of a handler that supports the input. | ||
* It rejects if no handlers support the given data. | ||
*/ | ||
public async handle (input: TIn): Promise<TOut> { | ||
let handler: AsyncHandler<TIn, TOut>; | ||
|
||
try { | ||
handler = await this.findHandler(input); | ||
} catch (error) { | ||
throw new Error('All handlers failed. This might be the consequence of calling handle before canHandle.'); | ||
} | ||
|
||
return handler.handle(input); | ||
} | ||
|
||
/** | ||
* Identical to {@link AsyncHandler.handleSafe} but optimized for composite by only needing 1 canHandle call on members. | ||
* @param input - The input data. | ||
* | ||
* @returns A promise corresponding to the handle call of a handler that supports the input. | ||
* It rejects if no handlers support the given data. | ||
*/ | ||
public async handleSafe (input: TIn): Promise<TOut> { | ||
const handler = await this.findHandler(input); | ||
|
||
return handler.handle(input); | ||
} | ||
|
||
/** | ||
* Finds a handler that can handle the given input data. | ||
* Otherwise an error gets thrown. | ||
* | ||
* @param input - The input data. | ||
* | ||
* @returns A promise resolving to a handler that supports the data or otherwise rejecting. | ||
*/ | ||
private async findHandler (input: TIn): Promise<AsyncHandler<TIn, TOut>> { | ||
const errors: Error[] = []; | ||
|
||
for (const handler of this.handlers) { | ||
try { | ||
await handler.canHandle(input); | ||
|
||
return handler; | ||
} catch (error) { | ||
errors.push(error); | ||
} | ||
} | ||
|
||
const joined = errors.map((error: Error): string => error.message).join(', '); | ||
|
||
throw new UnsupportedHttpError(`No handler supports the given input: [${joined}].`); | ||
} | ||
} |
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,30 @@ | ||
import { AsyncHandler } from '../../../src/util/AsyncHandler'; | ||
import { StaticAsyncHandler } from '../../util/StaticAsyncHandler'; | ||
|
||
describe('An AsyncHandler', (): void => { | ||
it('calls canHandle and handle when handleSafe is called.', async (): Promise<void> => { | ||
const handlerTrue: AsyncHandler<any, any> = new StaticAsyncHandler(true, null); | ||
const canHandleFn = jest.fn(async (input: any): Promise<void> => input); | ||
const handleFn = jest.fn(async (input: any): Promise<any> => input); | ||
|
||
handlerTrue.canHandle = canHandleFn; | ||
handlerTrue.handle = handleFn; | ||
await expect(handlerTrue.handleSafe('test')).resolves.toBe('test'); | ||
expect(canHandleFn).toHaveBeenCalledTimes(1); | ||
expect(handleFn).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('does not call handle when canHandle errors during a handleSafe call.', async (): Promise<void> => { | ||
const handlerFalse: AsyncHandler<any, any> = new StaticAsyncHandler(false, null); | ||
const canHandleFn = jest.fn(async (): Promise<void> => { | ||
throw new Error('test'); | ||
}); | ||
const handleFn = jest.fn(async (input: any): Promise<any> => input); | ||
|
||
handlerFalse.canHandle = canHandleFn; | ||
handlerFalse.handle = handleFn; | ||
await expect(handlerFalse.handleSafe('test')).rejects.toThrow(Error); | ||
expect(canHandleFn).toHaveBeenCalledTimes(1); | ||
expect(handleFn).toHaveBeenCalledTimes(0); | ||
}); | ||
}); |
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,76 @@ | ||
import { AsyncHandler } from '../../../src/util/AsyncHandler'; | ||
import { CompositeAsyncHandler } from '../../../src/util/CompositeAsyncHandler'; | ||
import { StaticAsyncHandler } from '../../util/StaticAsyncHandler'; | ||
|
||
describe('A CompositeAsyncHandler', (): void => { | ||
describe('with no handlers', (): void => { | ||
it('can never handle data.', async (): Promise<void> => { | ||
const handler = new CompositeAsyncHandler([]); | ||
|
||
await expect(handler.canHandle(null)).rejects.toThrow(Error); | ||
}); | ||
|
||
it('errors if its handle function is called.', async (): Promise<void> => { | ||
const handler = new CompositeAsyncHandler([]); | ||
|
||
await expect(handler.handle(null)).rejects.toThrow(Error); | ||
}); | ||
}); | ||
|
||
describe('with multiple handlers', (): void => { | ||
let handlerTrue: AsyncHandler<any, any>; | ||
let handlerFalse: AsyncHandler<any, any>; | ||
let canHandleFn: jest.Mock<Promise<void>, [any]>; | ||
let handleFn: jest.Mock<Promise<void>, [any]>; | ||
|
||
beforeEach(async (): Promise<void> => { | ||
handlerTrue = new StaticAsyncHandler(true, null); | ||
handlerFalse = new StaticAsyncHandler(false, null); | ||
|
||
canHandleFn = jest.fn(async (input: any): Promise<any> => input); | ||
handleFn = jest.fn(async (input: any): Promise<any> => input); | ||
handlerTrue.canHandle = canHandleFn; | ||
handlerTrue.handle = handleFn; | ||
}); | ||
|
||
it('can handle data if a handler supports it.', async (): Promise<void> => { | ||
const handler = new CompositeAsyncHandler([ handlerFalse, handlerTrue ]); | ||
|
||
await expect(handler.canHandle(null)).resolves.toBeUndefined(); | ||
}); | ||
|
||
it('can not handle data if no handler supports it.', async (): Promise<void> => { | ||
const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]); | ||
|
||
await expect(handler.canHandle(null)).rejects.toThrow('[Not supported., Not supported.]'); | ||
}); | ||
|
||
it('handles data if a handler supports it.', async (): Promise<void> => { | ||
const handler = new CompositeAsyncHandler([ handlerFalse, handlerTrue ]); | ||
|
||
await expect(handler.handle('test')).resolves.toEqual('test'); | ||
expect(canHandleFn).toHaveBeenCalledTimes(1); | ||
expect(handleFn).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('errors if the handle function is called but no handler supports the data.', async (): Promise<void> => { | ||
const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]); | ||
|
||
await expect(handler.handle('test')).rejects.toThrow('All handlers failed.'); | ||
}); | ||
|
||
it('only calls the canHandle function once of its handlers when handleSafe is called.', async (): Promise<void> => { | ||
const handler = new CompositeAsyncHandler([ handlerFalse, handlerTrue ]); | ||
|
||
await expect(handler.handleSafe('test')).resolves.toEqual('test'); | ||
expect(canHandleFn).toHaveBeenCalledTimes(1); | ||
expect(handleFn).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('throws the same error as canHandle when calling handleSafe if no handler supports the data.', async (): Promise<void> => { | ||
const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]); | ||
|
||
await expect(handler.handleSafe(null)).rejects.toThrow('[Not supported., Not supported.]'); | ||
}); | ||
}); | ||
}); |
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,24 @@ | ||
import { AsyncHandler } from '../../src/util/AsyncHandler'; | ||
|
||
export class StaticAsyncHandler<TOut> extends AsyncHandler<any, TOut> { | ||
private readonly canHandleStatic: boolean; | ||
|
||
private readonly handleStatic: TOut; | ||
|
||
public constructor (canHandleStatic: boolean, handleStatic: TOut) { | ||
super(); | ||
this.canHandleStatic = canHandleStatic; | ||
this.handleStatic = handleStatic; | ||
} | ||
|
||
public async canHandle (): Promise<void> { | ||
if (this.canHandleStatic) { | ||
return; | ||
} | ||
throw new Error('Not supported.'); | ||
} | ||
|
||
public async handle (): Promise<TOut> { | ||
return this.handleStatic; | ||
} | ||
} |