/
ChainedConverter.ts
101 lines (88 loc) · 3.76 KB
/
ChainedConverter.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import type { Representation } from '../../ldp/representation/Representation';
import { matchingMediaType } from '../../util/Util';
import { checkRequest } from './ConversionUtil';
import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
/**
* A meta converter that takes an array of other converters as input.
* It chains these converters by finding intermediate types that are supported by converters on either side.
*/
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.
* @param converters - The chain of converters.
*/
public constructor(converters: TypedRepresentationConverter[]) {
super();
if (converters.length < 2) {
throw new Error('At least 2 converters are required.');
}
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
// So we only check if the input can be parsed and the preferred type can be written
const inTypes = this.filterTypes(await this.first.getInputTypes());
const outTypes = this.filterTypes(await this.last.getOutputTypes());
checkRequest(input, inTypes, outTypes);
}
private filterTypes(typeVals: { [contentType: string]: number }): string[] {
return Object.keys(typeVals).filter((name): boolean => typeVals[name] > 0);
}
public async handle(input: RepresentationConverterArgs): Promise<Representation> {
const args = { ...input };
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);
}
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;
}
}