mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: add CompositeAsyncHandler to support multiple handlers
This commit is contained in:
parent
57405f3e26
commit
4229932a3a
@ -7,18 +7,22 @@ module.exports = {
|
|||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.ts$": "ts-jest"
|
"^.+\\.ts$": "ts-jest"
|
||||||
},
|
},
|
||||||
"testRegex": "/test/.*\\.ts$",
|
"testRegex": "/test/.*\\.test\\.ts$",
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
"ts",
|
"ts",
|
||||||
"js"
|
"js"
|
||||||
],
|
],
|
||||||
"testEnvironment": "node",
|
"testEnvironment": "node",
|
||||||
"collectCoverage": true,
|
"collectCoverage": true,
|
||||||
// either we don't build the test files (but then eslint needs a separate tsconfig) or we do this
|
|
||||||
"testPathIgnorePatterns": [
|
|
||||||
".*\\.d\\.ts"
|
|
||||||
],
|
|
||||||
"coveragePathIgnorePatterns": [
|
"coveragePathIgnorePatterns": [
|
||||||
"/node_modules/"
|
"/node_modules/"
|
||||||
]
|
],
|
||||||
|
"coverageThreshold": {
|
||||||
|
"./src": {
|
||||||
|
"branches": 100,
|
||||||
|
"functions": 100,
|
||||||
|
"lines": 100,
|
||||||
|
"statements": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
24
package-lock.json
generated
24
package-lock.json
generated
@ -5798,9 +5798,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ts-jest": {
|
"ts-jest": {
|
||||||
"version": "25.5.1",
|
"version": "26.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.0.0.tgz",
|
||||||
"integrity": "sha512-kHEUlZMK8fn8vkxDjwbHlxXRB9dHYpyzqKIGDNxbzs+Rz+ssNDSDNusEK8Fk/sDd4xE6iKoQLfFkFVaskmTJyw==",
|
"integrity": "sha512-eBpWH65mGgzobuw7UZy+uPP9lwu+tPp60o324ASRX4Ijg8UC5dl2zcge4kkmqr2Zeuk9FwIjvCTOPuNMEyGWWw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"bs-logger": "0.x",
|
"bs-logger": "0.x",
|
||||||
@ -5810,9 +5810,23 @@
|
|||||||
"lodash.memoize": "4.x",
|
"lodash.memoize": "4.x",
|
||||||
"make-error": "1.x",
|
"make-error": "1.x",
|
||||||
"micromatch": "4.x",
|
"micromatch": "4.x",
|
||||||
"mkdirp": "0.x",
|
"mkdirp": "1.x",
|
||||||
"semver": "6.x",
|
"semver": "7.x",
|
||||||
"yargs-parser": "18.x"
|
"yargs-parser": "18.x"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"mkdirp": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "7.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
|
||||||
|
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tslib": {
|
"tslib": {
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"pre-commit": "npm run lint && npm run build"
|
"pre-commit": "npm run build && npm run lint && npm run test"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@ -34,7 +34,7 @@
|
|||||||
"eslint-plugin-tsdoc": "^0.2.4",
|
"eslint-plugin-tsdoc": "^0.2.4",
|
||||||
"husky": "^4.2.5",
|
"husky": "^4.2.5",
|
||||||
"jest": "^26.0.1",
|
"jest": "^26.0.1",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^26.0.0",
|
||||||
"typescript": "^3.9.2"
|
"typescript": "^3.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,4 +6,4 @@ import { HttpRequest } from '../server/HttpRequest';
|
|||||||
* Responsible for extracting credentials from an incoming request.
|
* Responsible for extracting credentials from an incoming request.
|
||||||
* Will return `null` if no credentials were found.
|
* Will return `null` if no credentials were found.
|
||||||
*/
|
*/
|
||||||
export type CredentialsExtractor = AsyncHandler<HttpRequest, Credentials>;
|
export abstract class CredentialsExtractor extends AsyncHandler<HttpRequest, Credentials> {}
|
||||||
|
@ -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.
|
* 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.
|
* 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 {
|
export interface AuthorizerArgs {
|
||||||
/**
|
/**
|
||||||
|
@ -5,4 +5,4 @@ import { Operation } from '../operations/Operation';
|
|||||||
/**
|
/**
|
||||||
* Converts an incoming HttpRequest to an Operation.
|
* Converts an incoming HttpRequest to an Operation.
|
||||||
*/
|
*/
|
||||||
export type RequestParser = AsyncHandler<HttpRequest, Operation>;
|
export abstract class RequestParser extends AsyncHandler<HttpRequest, Operation> {}
|
||||||
|
@ -4,4 +4,4 @@ import { Operation } from './Operation';
|
|||||||
/**
|
/**
|
||||||
* Handler for a specific operation type.
|
* Handler for a specific operation type.
|
||||||
*/
|
*/
|
||||||
export type OperationHandler = AsyncHandler<Operation>;
|
export abstract class OperationHandler extends AsyncHandler<Operation> {}
|
||||||
|
@ -5,4 +5,4 @@ import { PermissionSet } from './PermissionSet';
|
|||||||
/**
|
/**
|
||||||
* Verifies which permissions are requested on a given {@link Operation}.
|
* Verifies which permissions are requested on a given {@link Operation}.
|
||||||
*/
|
*/
|
||||||
export type PermissionsExtractor = AsyncHandler<Operation, PermissionSet>;
|
export abstract class PermissionsExtractor extends AsyncHandler<Operation, PermissionSet> {}
|
||||||
|
@ -5,4 +5,4 @@ import { HttpResponse } from './HttpResponse';
|
|||||||
/**
|
/**
|
||||||
* An HTTP request handler.
|
* An HTTP request handler.
|
||||||
*/
|
*/
|
||||||
export type HttpHandler = AsyncHandler<{ request: HttpRequest; response: HttpResponse }>;
|
export abstract class HttpHandler extends AsyncHandler<{ request: HttpRequest; response: HttpResponse }> {}
|
||||||
|
@ -1,17 +1,35 @@
|
|||||||
/**
|
/**
|
||||||
* Simple interface for classes that can potentially handle a specific kind of data asynchronously.
|
* 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.
|
* 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.
|
* @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`.
|
* 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.
|
* @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.
|
* @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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
86
src/util/CompositeAsyncHandler.ts
Normal file
86
src/util/CompositeAsyncHandler.ts
Normal 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}].`);
|
||||||
|
}
|
||||||
|
}
|
30
test/unit/util/AsyncHandler.test.ts
Normal file
30
test/unit/util/AsyncHandler.test.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
76
test/unit/util/CompositeAsyncHandler.test.ts
Normal file
76
test/unit/util/CompositeAsyncHandler.test.ts
Normal file
@ -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.]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
24
test/util/StaticAsyncHandler.ts
Normal file
24
test/util/StaticAsyncHandler.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user