-
Notifications
You must be signed in to change notification settings - Fork 131
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Dynamically determine matching types in ChainedConverter
- Loading branch information
Showing
7 changed files
with
178 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters