Skip to content

Commit

Permalink
refactor: Create multiple composite handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Nov 6, 2020
1 parent 1260c5c commit 840965c
Show file tree
Hide file tree
Showing 16 changed files with 100 additions and 44 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ All notable changes to this project will be documented in this file.
* [feat: add typed readable](https://github.com/solid/community-server/commit/e0d74fd68af3575f267f8abc87c51a6fbab28d12)
* [feat: Add README with architecture links](https://github.com/solid/community-server/commit/aaf3f8e3aa890219e2a147622605ba2b62b729ee)
* [feat: add AuthenticatedLdpHandler](https://github.com/solid/community-server/commit/3e2cfaf11ee13c2ae3cb3e46f4df78c13c9d19cf)
* [feat: add CompositeAsyncHandler to support multiple handlers](https://github.com/solid/community-server/commit/4229932a3ac75c2532da4e495e96b779fc5b6c92)
* [feat: add FirstCompositeHandler to support multiple handlers](https://github.com/solid/community-server/commit/4229932a3ac75c2532da4e495e96b779fc5b6c92)
* [feat: add custom errors](https://github.com/solid/community-server/commit/57405f3e2695f3a82628e02052695314d656af95)
* [feat: add additional supported interfaces](https://github.com/solid/community-server/commit/a4f2b3995c3e8cfeacf5fe3dbbc0eeb8020f9c9e)
* [Initial configuration](https://github.com/solid/community-server/commit/b949b6cf5eade549b91731edcd1c4d931537a42e)
4 changes: 2 additions & 2 deletions config/presets/ldp/operation-handler.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"@graph": [
{
"@id": "urn:solid-server:default:OperationHandler",
"@type": "CompositeAsyncHandler",
"CompositeAsyncHandler:_handlers": [
"@type": "FirstCompositeHandler",
"FirstCompositeHandler:_handlers": [
{
"@type": "DeleteOperationHandler",
"DeleteOperationHandler:_store": {
Expand Down
4 changes: 2 additions & 2 deletions config/presets/ldp/permissions-extractor.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"@graph": [
{
"@id": "urn:solid-server:default:PermissionsExtractor",
"@type": "CompositeAsyncHandler",
"CompositeAsyncHandler:_handlers": [
"@type": "FirstCompositeHandler",
"FirstCompositeHandler:_handlers": [
{
"@type": "MethodPermissionsExtractor"
},
Expand Down
4 changes: 2 additions & 2 deletions config/presets/ldp/request-parser.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"@id": "urn:solid-server:default:MetadataExtractor"
},
"BasicRequestParser:_bodyParser": {
"@type": "CompositeAsyncHandler",
"CompositeAsyncHandler:_handlers": [
"@type": "FirstCompositeHandler",
"FirstCompositeHandler:_handlers": [
{
"@type": "SparqlUpdateBodyParser"
},
Expand Down
4 changes: 2 additions & 2 deletions config/presets/ldp/response-writer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"@graph": [
{
"@id": "urn:solid-server:default:ResponseWriter",
"@type": "CompositeAsyncHandler",
"CompositeAsyncHandler:_handlers": [
"@type": "FirstCompositeHandler",
"FirstCompositeHandler:_handlers": [
{
"@type": "ErrorResponseWriter"
},
Expand Down
4 changes: 2 additions & 2 deletions config/presets/representation-conversion.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@

{
"@id": "urn:solid-server:default:RepresentationConverter",
"@type": "CompositeAsyncHandler",
"CompositeAsyncHandler:_handlers": [
"@type": "FirstCompositeHandler",
"FirstCompositeHandler:_handlers": [
{
"@id": "urn:solid-server:default:RdfToQuadConverter"
},
Expand Down
5 changes: 3 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,9 @@ export * from './src/util/errors/UnsupportedHttpError';
export * from './src/util/errors/UnsupportedMediaTypeHttpError';

// Util
export * from './src/util/HeaderUtil';
export * from './src/util/AllVoidCompositeHandler';
export * from './src/util/AsyncHandler';
export * from './src/util/CompositeAsyncHandler';
export * from './src/util/FirstCompositeHandler';
export * from './src/util/HeaderUtil';
export * from './src/util/MetadataController';
export * from './src/util/Util';
24 changes: 24 additions & 0 deletions src/util/AllVoidCompositeHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { AsyncHandler } from './AsyncHandler';

/**
* A composite handler that runs all of its handlers independent of their result.
* The `canHandle` check of this handler will always succeed.
*/
export class AllVoidCompositeHandler<TIn> extends AsyncHandler<TIn> {
private readonly handlers: AsyncHandler<TIn>[];

public constructor(handlers: AsyncHandler<TIn>[]) {
super();
this.handlers = handlers;
}

public async handle(input: TIn): Promise<void> {
for (const handler of this.handlers) {
try {
await handler.handleSafe(input);
} catch {
// Ignore errors
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import { UnsupportedHttpError } from './errors/UnsupportedHttpError';
* The handlers will be checked in the order they appear in the input array,
* allowing for more fine-grained handlers to check before catch-all handlers.
*/
export class CompositeAsyncHandler<TIn, TOut> implements AsyncHandler<TIn, TOut> {
export class FirstCompositeHandler<TIn, TOut> implements AsyncHandler<TIn, TOut> {
protected readonly logger = getLoggerFor(this);

private readonly handlers: AsyncHandler<TIn, TOut>[];

/**
* Creates a new CompositeAsyncHandler that stores the given handlers.
* Creates a new FirstCompositeHandler that stores the given handlers.
* @param handlers - Handlers over which it will run.
*/
public constructor(handlers: AsyncHandler<TIn, TOut>[]) {
Expand Down
4 changes: 2 additions & 2 deletions test/configs/AuthenticatedDataAccessorBasedConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
} from '../../index';
import {
AuthenticatedLdpHandler,
CompositeAsyncHandler,
FirstCompositeHandler,
MethodPermissionsExtractor,
RdfToQuadConverter,
UnsecureWebIdExtractor,
Expand Down Expand Up @@ -44,7 +44,7 @@ export class AuthenticatedDataAccessorBasedConfig implements ServerConfig {
const requestParser = getBasicRequestParser();

const credentialsExtractor = new UnsecureWebIdExtractor();
const permissionsExtractor = new CompositeAsyncHandler([
const permissionsExtractor = new FirstCompositeHandler([
new MethodPermissionsExtractor(),
]);

Expand Down
4 changes: 2 additions & 2 deletions test/configs/BasicHandlersConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { HttpHandler,
import {
AllowEverythingAuthorizer,
AuthenticatedLdpHandler,
CompositeAsyncHandler,
FirstCompositeHandler,
MethodPermissionsExtractor,
QuadToRdfConverter,
RawBodyParser,
Expand Down Expand Up @@ -48,7 +48,7 @@ export class BasicHandlersConfig implements ServerConfig {
]);

const credentialsExtractor = new UnsecureWebIdExtractor();
const permissionsExtractor = new CompositeAsyncHandler([
const permissionsExtractor = new FirstCompositeHandler([
new MethodPermissionsExtractor(),
new SparqlPatchPermissionsExtractor(),
]);
Expand Down
4 changes: 2 additions & 2 deletions test/configs/BasicHandlersWithAclConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { HttpHandler,
ResourceStore } from '../../index';
import {
AuthenticatedLdpHandler,
CompositeAsyncHandler,
FirstCompositeHandler,
MethodPermissionsExtractor,
RdfToQuadConverter,
UnsecureWebIdExtractor,
Expand Down Expand Up @@ -40,7 +40,7 @@ export class BasicHandlersWithAclConfig implements ServerConfig {
const requestParser = getBasicRequestParser();

const credentialsExtractor = new UnsecureWebIdExtractor();
const permissionsExtractor = new CompositeAsyncHandler([
const permissionsExtractor = new FirstCompositeHandler([
new MethodPermissionsExtractor(),
]);

Expand Down
4 changes: 2 additions & 2 deletions test/configs/DataAccessorBasedConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
import {
AllowEverythingAuthorizer,
AuthenticatedLdpHandler,
CompositeAsyncHandler,
FirstCompositeHandler,
MethodPermissionsExtractor,
QuadToRdfConverter,
RawBodyParser,
Expand Down Expand Up @@ -44,7 +44,7 @@ export class DataAccessorBasedConfig implements ServerConfig {
const requestParser = getBasicRequestParser([ new RawBodyParser() ]);

const credentialsExtractor = new UnsecureWebIdExtractor();
const permissionsExtractor = new CompositeAsyncHandler([
const permissionsExtractor = new FirstCompositeHandler([
new MethodPermissionsExtractor(),
]);
const authorizer = new AllowEverythingAuthorizer();
Expand Down
15 changes: 8 additions & 7 deletions test/configs/Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import type {
ResponseDescription,
HttpResponse,
ResponseWriter,
OperationHandler,
} from '../../index';
import {
AcceptPreferenceParser,
BasicMetadataExtractor,
BasicRequestParser,
BasicResponseWriter,
BasicTargetExtractor,
CompositeAsyncHandler,
FirstCompositeHandler,
ContentTypeParser,
DataAccessorBasedStore,
DeleteOperationHandler,
Expand Down Expand Up @@ -82,8 +83,8 @@ export const getConvertingStore =
(store: ResourceStore, converters: RepresentationConverter[], inType?: string):
RepresentationConvertingStore =>
new RepresentationConvertingStore(store, {
inConverter: new CompositeAsyncHandler(converters),
outConverter: new CompositeAsyncHandler(converters),
inConverter: new FirstCompositeHandler(converters),
outConverter: new FirstCompositeHandler(converters),
inType,
});

Expand All @@ -105,7 +106,7 @@ export const getPatchingStore = (store: ResourceStore): PatchingStore => {
*
* @returns The operation handler.
*/
export const getOperationHandler = (store: ResourceStore): CompositeAsyncHandler<Operation, ResponseDescription> => {
export const getOperationHandler = (store: ResourceStore): OperationHandler => {
const handlers = [
new GetOperationHandler(store),
new HeadOperationHandler(store),
Expand All @@ -114,11 +115,11 @@ export const getOperationHandler = (store: ResourceStore): CompositeAsyncHandler
new PatchOperationHandler(store),
new DeleteOperationHandler(store),
];
return new CompositeAsyncHandler<Operation, ResponseDescription>(handlers);
return new FirstCompositeHandler<Operation, ResponseDescription>(handlers);
};

export const getResponseWriter = (): ResponseWriter =>
new CompositeAsyncHandler<{ response: HttpResponse; result: ResponseDescription | Error }, void>([
new FirstCompositeHandler<{ response: HttpResponse; result: ResponseDescription | Error }, void>([
new ErrorResponseWriter(),
new BasicResponseWriter(),
]);
Expand Down Expand Up @@ -146,7 +147,7 @@ export const getBasicRequestParser = (bodyParsers: BodyParser[] = []): BasicRequ
// If no body parser is given (array is empty), default to RawBodyParser
bodyParser = new RawBodyParser();
} else {
bodyParser = new CompositeAsyncHandler(bodyParsers);
bodyParser = new FirstCompositeHandler(bodyParsers);
}
return new BasicRequestParser({
targetExtractor: new BasicTargetExtractor(),
Expand Down
30 changes: 30 additions & 0 deletions test/unit/util/AllVoidCompositeHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { AllVoidCompositeHandler } from '../../../src/util/AllVoidCompositeHandler';
import type { AsyncHandler } from '../../../src/util/AsyncHandler';

describe('An AllVoidCompositeHandler', (): void => {
let handler1: AsyncHandler<string>;
let handler2: AsyncHandler<string>;
let composite: AllVoidCompositeHandler<string>;

beforeEach(async(): Promise<void> => {
handler1 = { handleSafe: jest.fn() } as any;
handler2 = { handleSafe: jest.fn() } as any;

composite = new AllVoidCompositeHandler<string>([ handler1, handler2 ]);
});

it('can handle all input.', async(): Promise<void> => {
await expect(composite.canHandle('test')).resolves.toBeUndefined();
});

it('runs all handlers without caring about their result.', async(): Promise<void> => {
handler1.handleSafe = jest.fn(async(): Promise<void> => {
throw new Error('error');
});
await expect(composite.handleSafe('test')).resolves.toBeUndefined();
expect(handler1.handleSafe).toHaveBeenCalledTimes(1);
expect(handler1.handleSafe).toHaveBeenLastCalledWith('test');
expect(handler2.handleSafe).toHaveBeenCalledTimes(1);
expect(handler2.handleSafe).toHaveBeenLastCalledWith('test');
});
});
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import type { AsyncHandler } from '../../../src/util/AsyncHandler';
import { CompositeAsyncHandler } from '../../../src/util/CompositeAsyncHandler';
import { HttpError } from '../../../src/util/errors/HttpError';
import { UnsupportedHttpError } from '../../../src/util/errors/UnsupportedHttpError';
import { FirstCompositeHandler } from '../../../src/util/FirstCompositeHandler';
import { StaticAsyncHandler } from '../../util/StaticAsyncHandler';

describe('A CompositeAsyncHandler', (): void => {
describe('A FirstCompositeHandler', (): void => {
describe('with no handlers', (): void => {
it('can never handle data.', async(): Promise<void> => {
const handler = new CompositeAsyncHandler([]);
const handler = new FirstCompositeHandler([]);

await expect(handler.canHandle(null)).rejects.toThrow(Error);
});

it('errors if its handle function is called.', async(): Promise<void> => {
const handler = new CompositeAsyncHandler([]);
const handler = new FirstCompositeHandler([]);

await expect(handler.handle(null)).rejects.toThrow(Error);
});
Expand All @@ -36,13 +36,13 @@ describe('A CompositeAsyncHandler', (): void => {
});

it('can handle data if a handler supports it.', async(): Promise<void> => {
const handler = new CompositeAsyncHandler([ handlerFalse, handlerTrue ]);
const handler = new FirstCompositeHandler([ 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 ]);
const handler = new FirstCompositeHandler([ handlerFalse, handlerFalse ]);

await expect(handler.canHandle(null)).rejects.toThrow('[Not supported, Not supported]');
});
Expand All @@ -51,35 +51,35 @@ describe('A CompositeAsyncHandler', (): void => {
handlerFalse.canHandle = async(): Promise<void> => {
throw 'apple';
};
const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]);
const handler = new FirstCompositeHandler([ handlerFalse, handlerFalse ]);

await expect(handler.canHandle(null)).rejects.toThrow('[Unknown error, Unknown error]');
});

it('handles data if a handler supports it.', async(): Promise<void> => {
const handler = new CompositeAsyncHandler([ handlerFalse, handlerTrue ]);
const handler = new FirstCompositeHandler([ 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 ]);
const handler = new FirstCompositeHandler([ 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 ]);
const handler = new FirstCompositeHandler([ handlerFalse, handlerTrue ]);

await expect(handler.handleSafe('test')).resolves.toEqual('test');
expect(canHandleFn).toHaveBeenCalledTimes(1);
expect(handleFn).toHaveBeenCalledTimes(1);
});

it('throws the canHandle error when calling handleSafe if the data is not supported.', async(): Promise<void> => {
const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]);
const handler = new FirstCompositeHandler([ handlerFalse, handlerFalse ]);

await expect(handler.handleSafe(null)).rejects.toThrow('[Not supported, Not supported]');
});
Expand All @@ -88,7 +88,7 @@ describe('A CompositeAsyncHandler', (): void => {
handlerTrue.canHandle = async(): Promise<void> => {
throw new HttpError(401, 'UnauthorizedHttpError');
};
const handler = new CompositeAsyncHandler([ handlerTrue, handlerTrue ]);
const handler = new FirstCompositeHandler([ handlerTrue, handlerTrue ]);

await expect(handler.canHandle(null)).rejects.toMatchObject({
statusCode: 401,
Expand All @@ -103,7 +103,7 @@ describe('A CompositeAsyncHandler', (): void => {
handlerFalse.canHandle = async(): Promise<void> => {
throw new Error('Server is crashing!');
};
const handler = new CompositeAsyncHandler([ handlerTrue, handlerFalse ]);
const handler = new FirstCompositeHandler([ handlerTrue, handlerFalse ]);

await expect(handler.canHandle(null)).rejects.toMatchObject({
statusCode: 500,
Expand All @@ -118,7 +118,7 @@ describe('A CompositeAsyncHandler', (): void => {
handlerFalse.canHandle = async(): Promise<void> => {
throw new HttpError(415, 'UnsupportedMediaTypeHttpError');
};
const handler = new CompositeAsyncHandler([ handlerTrue, handlerFalse ]);
const handler = new FirstCompositeHandler([ handlerTrue, handlerFalse ]);

await expect(handler.canHandle(null)).rejects.toThrow(UnsupportedHttpError);
});
Expand Down

0 comments on commit 840965c

Please sign in to comment.