mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Create RepresentationConverter that chains other converters
This commit is contained in:
parent
a1e9410365
commit
734f7e7f0f
53
src/storage/conversion/ChainedConverter.ts
Normal file
53
src/storage/conversion/ChainedConverter.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Representation } from '../../ldp/representation/Representation';
|
||||||
|
import { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences';
|
||||||
|
import { RepresentationConverter, RepresentationConverterArgs } from './RepresentationConverter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A meta converter that takes an array of other converters as input.
|
||||||
|
* It chains these converters based on given intermediate types that are supported by converters on either side.
|
||||||
|
*/
|
||||||
|
export class ChainedConverter extends RepresentationConverter {
|
||||||
|
private readonly converters: RepresentationConverter[];
|
||||||
|
private readonly chainTypes: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the chain of converters based on the input.
|
||||||
|
* The list of `converters` needs to be at least 2 long,
|
||||||
|
* and `chainTypes` needs to be the same length - 1,
|
||||||
|
* as each type at index `i` corresponds to the output type of converter `i`
|
||||||
|
* and input type of converter `i+1`.
|
||||||
|
* @param converters - The chain of converters.
|
||||||
|
* @param chainTypes - The intermediate types of the chain.
|
||||||
|
*/
|
||||||
|
public constructor(converters: RepresentationConverter[], chainTypes: string[]) {
|
||||||
|
super();
|
||||||
|
if (converters.length < 2) {
|
||||||
|
throw new Error('At least 2 converters are required.');
|
||||||
|
}
|
||||||
|
if (chainTypes.length !== converters.length - 1) {
|
||||||
|
throw new Error('1 type is required per converter chain.');
|
||||||
|
}
|
||||||
|
this.converters = converters;
|
||||||
|
this.chainTypes = chainTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async canHandle(input: RepresentationConverterArgs): Promise<void> {
|
||||||
|
// Check if the first converter can handle the input
|
||||||
|
const preferences: RepresentationPreferences = { type: [{ value: this.chainTypes[0], weight: 1 }]};
|
||||||
|
await this.converters[0].canHandle({ ...input, preferences });
|
||||||
|
|
||||||
|
// Check if the last converter can produce the output
|
||||||
|
const representation: Representation = { ...input.representation };
|
||||||
|
representation.metadata = { ...input.representation.metadata, contentType: this.chainTypes.slice(-1)[0] };
|
||||||
|
await this.converters.slice(-1)[0].canHandle({ ...input, representation });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handle(input: RepresentationConverterArgs): Promise<Representation> {
|
||||||
|
const args = { ...input };
|
||||||
|
for (let i = 0; i < this.chainTypes.length; ++i) {
|
||||||
|
args.preferences = { type: [{ value: this.chainTypes[i], weight: 1 }]};
|
||||||
|
args.representation = await this.converters[i].handle(args);
|
||||||
|
}
|
||||||
|
return this.converters.slice(-1)[0].handle(args);
|
||||||
|
}
|
||||||
|
}
|
85
test/unit/storage/conversion/ChainedConverter.test.ts
Normal file
85
test/unit/storage/conversion/ChainedConverter.test.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { Representation } from '../../../../src/ldp/representation/Representation';
|
||||||
|
import { RepresentationPreferences } from '../../../../src/ldp/representation/RepresentationPreferences';
|
||||||
|
import { ChainedConverter } from '../../../../src/storage/conversion/ChainedConverter';
|
||||||
|
import { checkRequest } from '../../../../src/storage/conversion/ConversionUtil';
|
||||||
|
import {
|
||||||
|
RepresentationConverter,
|
||||||
|
RepresentationConverterArgs,
|
||||||
|
} from '../../../../src/storage/conversion/RepresentationConverter';
|
||||||
|
|
||||||
|
class DummyConverter extends RepresentationConverter {
|
||||||
|
private readonly inType: string;
|
||||||
|
private readonly outType: string;
|
||||||
|
|
||||||
|
public constructor(inType: string, outType: string) {
|
||||||
|
super();
|
||||||
|
this.inType = inType;
|
||||||
|
this.outType = outType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async canHandle(input: RepresentationConverterArgs): Promise<void> {
|
||||||
|
checkRequest(input, [ this.inType ], [ this.outType ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handle(input: RepresentationConverterArgs): Promise<Representation> {
|
||||||
|
const representation: Representation = { ...input.representation };
|
||||||
|
representation.metadata = { ...input.representation.metadata, contentType: this.outType };
|
||||||
|
return representation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('A ChainedConverter', (): void => {
|
||||||
|
let converters: RepresentationConverter[];
|
||||||
|
let converter: ChainedConverter;
|
||||||
|
let representation: Representation;
|
||||||
|
let preferences: RepresentationPreferences;
|
||||||
|
let args: RepresentationConverterArgs;
|
||||||
|
|
||||||
|
beforeEach(async(): Promise<void> => {
|
||||||
|
converters = [
|
||||||
|
new DummyConverter('text/turtle', 'chain/1'),
|
||||||
|
new DummyConverter('chain/1', 'chain/2'),
|
||||||
|
new DummyConverter('chain/2', 'internal/quads'),
|
||||||
|
];
|
||||||
|
converter = new ChainedConverter(converters, [ 'chain/1', 'chain/2' ]);
|
||||||
|
|
||||||
|
representation = { metadata: { contentType: 'text/turtle' } as any } as Representation;
|
||||||
|
preferences = { type: [{ value: 'internal/quads', weight: 1 }]};
|
||||||
|
args = { representation, preferences, identifier: { path: 'path' }};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('needs at least 2 converter and n-1 chains.', async(): Promise<void> => {
|
||||||
|
expect((): any => new ChainedConverter([], [])).toThrow('At least 2 converters are required.');
|
||||||
|
expect((): any => new ChainedConverter([ converters[0] ], [])).toThrow('At least 2 converters are required.');
|
||||||
|
expect((): any => new ChainedConverter([ converters[0], converters[1] ], []))
|
||||||
|
.toThrow('1 type is required per converter chain.');
|
||||||
|
expect(new ChainedConverter([ converters[0], converters[1] ], [ 'apple' ]))
|
||||||
|
.toBeInstanceOf(ChainedConverter);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can handle requests with the correct in- and output.', async(): Promise<void> => {
|
||||||
|
await expect(converter.canHandle(args)).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if the start of the chain does not support the representation type.', async(): Promise<void> => {
|
||||||
|
representation.metadata.contentType = 'bad/type';
|
||||||
|
await expect(converter.canHandle(args)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if the end of the chain does not support the preferences.', async(): Promise<void> => {
|
||||||
|
delete preferences.type;
|
||||||
|
await expect(converter.canHandle(args)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runs the data through the chain.', async(): Promise<void> => {
|
||||||
|
jest.spyOn(converters[0], 'handle');
|
||||||
|
jest.spyOn(converters[1], 'handle');
|
||||||
|
jest.spyOn(converters[2], 'handle');
|
||||||
|
|
||||||
|
const result = await converter.handle(args);
|
||||||
|
expect(result.metadata.contentType).toEqual('internal/quads');
|
||||||
|
expect((converters[0] as any).handle).toHaveBeenCalledTimes(1);
|
||||||
|
expect((converters[1] as any).handle).toHaveBeenCalledTimes(1);
|
||||||
|
expect((converters[2] as any).handle).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user