feat: add CompositeAsyncHandler to support multiple handlers

This commit is contained in:
Joachim Van Herwegen
2020-05-28 10:55:29 +02:00
parent 57405f3e26
commit 4229932a3a
14 changed files with 275 additions and 23 deletions

View File

@@ -6,4 +6,4 @@ import { HttpRequest } from '../server/HttpRequest';
* Responsible for extracting credentials from an incoming request.
* Will return `null` if no credentials were found.
*/
export type CredentialsExtractor = AsyncHandler<HttpRequest, Credentials>;
export abstract class CredentialsExtractor extends AsyncHandler<HttpRequest, Credentials> {}

View File

@@ -7,7 +7,7 @@ import { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
* Verifies if the given credentials have access to the given permissions on the given resource.
* An {@link Error} with the necessary explanation will be thrown when permissions are not granted.
*/
export type Authorizer = AsyncHandler<AuthorizerArgs>;
export abstract class Authorizer extends AsyncHandler<AuthorizerArgs> {}
export interface AuthorizerArgs {
/**

View File

@@ -5,4 +5,4 @@ import { Operation } from '../operations/Operation';
/**
* Converts an incoming HttpRequest to an Operation.
*/
export type RequestParser = AsyncHandler<HttpRequest, Operation>;
export abstract class RequestParser extends AsyncHandler<HttpRequest, Operation> {}

View File

@@ -4,4 +4,4 @@ import { Operation } from './Operation';
/**
* Handler for a specific operation type.
*/
export type OperationHandler = AsyncHandler<Operation>;
export abstract class OperationHandler extends AsyncHandler<Operation> {}

View File

@@ -5,4 +5,4 @@ import { PermissionSet } from './PermissionSet';
/**
* Verifies which permissions are requested on a given {@link Operation}.
*/
export type PermissionsExtractor = AsyncHandler<Operation, PermissionSet>;
export abstract class PermissionsExtractor extends AsyncHandler<Operation, PermissionSet> {}

View File

@@ -5,4 +5,4 @@ import { HttpResponse } from './HttpResponse';
/**
* An HTTP request handler.
*/
export type HttpHandler = AsyncHandler<{ request: HttpRequest; response: HttpResponse }>;
export abstract class HttpHandler extends AsyncHandler<{ request: HttpRequest; response: HttpResponse }> {}

View File

@@ -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);
}
}

View File

@@ -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}].`);
}
}