feat: Dynamically determine matching types in ChainedConverter

This commit is contained in:
Joachim Van Herwegen
2020-09-01 14:02:50 +02:00
parent 734f7e7f0f
commit af4a82f4c1
7 changed files with 178 additions and 50 deletions

View File

@@ -1,53 +1,104 @@
import { Representation } from '../../ldp/representation/Representation';
import { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences';
import { RepresentationConverter, RepresentationConverterArgs } from './RepresentationConverter';
import { matchingMediaType } from '../../util/Util';
import { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
/**
* 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.
* It chains these converters by finding intermediate types that are supported by converters on either side.
*/
export class ChainedConverter extends RepresentationConverter {
private readonly converters: RepresentationConverter[];
private readonly chainTypes: string[];
export class ChainedConverter extends TypedRepresentationConverter {
private readonly converters: TypedRepresentationConverter[];
/**
* 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`.
* The list of `converters` needs to be at least 2 long.
* @param converters - The chain of converters.
* @param chainTypes - The intermediate types of the chain.
*/
public constructor(converters: RepresentationConverter[], chainTypes: string[]) {
public constructor(converters: TypedRepresentationConverter[]) {
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;
this.converters = [ ...converters ];
}
protected get first(): TypedRepresentationConverter {
return this.converters[0];
}
protected get last(): TypedRepresentationConverter {
return this.converters[this.converters.length - 1];
}
public async getInputTypes(): Promise<{ [contentType: string]: number }> {
return this.first.getInputTypes();
}
public async getOutputTypes(): Promise<{ [contentType: string]: number }> {
return this.last.getOutputTypes();
}
public async canHandle(input: RepresentationConverterArgs): Promise<void> {
// We assume a chain can be constructed, otherwise there would be a configuration issue
// 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 });
const firstChain = await this.getMatchingType(this.converters[0], this.converters[1]);
const preferences: RepresentationPreferences = { type: [{ value: firstChain, weight: 1 }]};
await this.first.canHandle({ ...input, preferences });
// Check if the last converter can produce the output
const idx = this.converters.length - 1;
const lastChain = await this.getMatchingType(this.converters[idx - 1], this.converters[idx]);
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 });
representation.metadata = { ...input.representation.metadata, contentType: lastChain };
await this.last.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 }]};
for (let i = 0; i < this.converters.length - 1; ++i) {
const value = await this.getMatchingType(this.converters[i], this.converters[i + 1]);
args.preferences = { type: [{ value, weight: 1 }]};
args.representation = await this.converters[i].handle(args);
}
return this.converters.slice(-1)[0].handle(args);
args.preferences = input.preferences;
return this.last.handle(args);
}
/**
* Finds the best media type that can be used to chain 2 converters.
*/
protected async getMatchingType(left: TypedRepresentationConverter, right: TypedRepresentationConverter):
Promise<string> {
const leftTypes = await left.getOutputTypes();
const rightTypes = await right.getInputTypes();
let bestMatch: { type: string; weight: number } = { type: 'invalid', weight: 0 };
// Try to find the matching type with the best weight
const leftKeys = Object.keys(leftTypes);
const rightKeys = Object.keys(rightTypes);
for (const leftType of leftKeys) {
const leftWeight = leftTypes[leftType];
if (leftWeight <= bestMatch.weight) {
continue;
}
for (const rightType of rightKeys) {
const rightWeight = rightTypes[rightType];
const weight = leftWeight * rightWeight;
if (weight > bestMatch.weight && matchingMediaType(leftType, rightType)) {
bestMatch = { type: leftType, weight };
if (weight === 1) {
return bestMatch.type;
}
}
}
}
if (bestMatch.weight === 0) {
throw new Error(`No match found between ${leftKeys} and ${rightKeys}`);
}
return bestMatch.type;
}
}

View File

@@ -5,12 +5,21 @@ import { RepresentationMetadata } from '../../ldp/representation/RepresentationM
import { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences';
import { CONTENT_TYPE_QUADS, DATA_TYPE_BINARY } from '../../util/ContentTypes';
import { checkRequest, matchingTypes } from './ConversionUtil';
import { RepresentationConverter, RepresentationConverterArgs } from './RepresentationConverter';
import { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
/**
* Converts `internal/quads` to most major RDF serializations.
*/
export class QuadToRdfConverter extends RepresentationConverter {
export class QuadToRdfConverter extends TypedRepresentationConverter {
public async getInputTypes(): Promise<{ [contentType: string]: number }> {
return { [CONTENT_TYPE_QUADS]: 1 };
}
public async getOutputTypes(): Promise<{ [contentType: string]: number }> {
return rdfSerializer.getContentTypesPrioritized();
}
public async canHandle(input: RepresentationConverterArgs): Promise<void> {
checkRequest(input, [ CONTENT_TYPE_QUADS ], await rdfSerializer.getContentTypes());
}

View File

@@ -5,12 +5,21 @@ import { RepresentationMetadata } from '../../ldp/representation/RepresentationM
import { CONTENT_TYPE_QUADS, DATA_TYPE_QUAD } from '../../util/ContentTypes';
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
import { checkRequest } from './ConversionUtil';
import { RepresentationConverter, RepresentationConverterArgs } from './RepresentationConverter';
import { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
/**
* Converts most major RDF serializations to `internal/quads`.
*/
export class RdfToQuadConverter extends RepresentationConverter {
export class RdfToQuadConverter extends TypedRepresentationConverter {
public async getInputTypes(): Promise<{ [contentType: string]: number }> {
return rdfParser.getContentTypesPrioritized();
}
public async getOutputTypes(): Promise<{ [contentType: string]: number }> {
return { [CONTENT_TYPE_QUADS]: 1 };
}
public async canHandle(input: RepresentationConverterArgs): Promise<void> {
checkRequest(input, await rdfParser.getContentTypes(), [ CONTENT_TYPE_QUADS ]);
}

View File

@@ -0,0 +1,20 @@
import { RepresentationConverter } from './RepresentationConverter';
/**
* A {@link RepresentationConverter} that allows requesting the supported types.
*/
export abstract class TypedRepresentationConverter extends RepresentationConverter {
/**
* Get a hash of all supported input content types for this converter, mapped to a numerical priority.
* The priority weight goes from 0 up to 1.
* @returns A promise resolving to a hash mapping content type to a priority number.
*/
public abstract getInputTypes(): Promise<{ [contentType: string]: number }>;
/**
* Get a hash of all supported output content types for this converter, mapped to a numerical priority.
* The priority weight goes from 0 up to 1.
* @returns A promise resolving to a hash mapping content type to a priority number.
*/
public abstract getOutputTypes(): Promise<{ [contentType: string]: number }>;
}