feat: Determine Typed Converter output based on input type

This commit is contained in:
Joachim Van Herwegen 2021-10-26 16:30:10 +02:00
parent 27306d6e3f
commit fa94c7d4bb
17 changed files with 107 additions and 134 deletions

View File

@ -1,8 +1,8 @@
import type { ValuePreferences } from '../../http/representation/RepresentationPreferences'; import type { ValuePreferences } from '../../http/representation/RepresentationPreferences';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { getConversionTarget, getTypeWeight } from './ConversionUtil'; import { getConversionTarget, getTypeWeight, preferencesToString } from './ConversionUtil';
import { RepresentationConverter } from './RepresentationConverter';
import type { RepresentationConverterArgs } from './RepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
type PromiseOrValue<T> = T | Promise<T>; type PromiseOrValue<T> = T | Promise<T>;
type ValuePreferencesArg = type ValuePreferencesArg =
@ -22,30 +22,37 @@ async function toValuePreferences(arg: ValuePreferencesArg): Promise<ValuePrefer
} }
/** /**
* A {@link RepresentationConverter} that allows requesting the supported types. * A base {@link TypedRepresentationConverter} implementation for converters
* that can convert from all its input types to all its output types.
*
* This base class handles the `canHandle` call by comparing the input content type to the stored input types
* and the output preferences to the stored output types.
*
* Output weights are determined by multiplying all stored output weights with the weight of the input type.
*/ */
export abstract class BaseTypedRepresentationConverter extends RepresentationConverter { export abstract class BaseTypedRepresentationConverter extends TypedRepresentationConverter {
protected inputTypes: Promise<ValuePreferences>; protected inputTypes: Promise<ValuePreferences>;
protected outputTypes: Promise<ValuePreferences>; protected outputTypes: Promise<ValuePreferences>;
public constructor(inputTypes: ValuePreferencesArg = {}, outputTypes: ValuePreferencesArg = {}) { public constructor(inputTypes: ValuePreferencesArg, outputTypes: ValuePreferencesArg) {
super(); super();
this.inputTypes = toValuePreferences(inputTypes); this.inputTypes = toValuePreferences(inputTypes);
this.outputTypes = toValuePreferences(outputTypes); this.outputTypes = toValuePreferences(outputTypes);
} }
/** /**
* Gets the supported input content types for this converter, mapped to a numerical priority. * Matches all inputs to all outputs.
*/ */
public async getInputTypes(): Promise<ValuePreferences> { public async getOutputTypes(contentType: string): Promise<ValuePreferences> {
return this.inputTypes; const weight = getTypeWeight(contentType, await this.inputTypes);
if (weight > 0) {
const outputTypes = { ...await this.outputTypes };
for (const [ key, value ] of Object.entries(outputTypes)) {
outputTypes[key] = value * weight;
} }
return outputTypes;
/** }
* Gets the supported output content types for this converter, mapped to a numerical quality. return {};
*/
public async getOutputTypes(): Promise<ValuePreferences> {
return this.outputTypes;
} }
/** /**
@ -57,19 +64,18 @@ export abstract class BaseTypedRepresentationConverter extends RepresentationCon
* Throws an error with details if conversion is not possible. * Throws an error with details if conversion is not possible.
*/ */
public async canHandle(args: RepresentationConverterArgs): Promise<void> { public async canHandle(args: RepresentationConverterArgs): Promise<void> {
const types = [ this.getInputTypes(), this.getOutputTypes() ];
const { contentType } = args.representation.metadata; const { contentType } = args.representation.metadata;
if (!contentType) { if (!contentType) {
throw new NotImplementedHttpError('Can not convert data without a Content-Type.'); throw new NotImplementedHttpError('Can not convert data without a Content-Type.');
} }
const [ inputTypes, outputTypes ] = await Promise.all(types); const outputTypes = await this.getOutputTypes(contentType);
const outputPreferences = args.preferences.type ?? {}; const outputPreferences = args.preferences.type ?? {};
if (getTypeWeight(contentType, inputTypes) === 0 || !getConversionTarget(outputTypes, outputPreferences)) { if (!getConversionTarget(outputTypes, outputPreferences)) {
throw new NotImplementedHttpError( throw new NotImplementedHttpError(
`Cannot convert from ${contentType} to ${Object.keys(outputPreferences) `Cannot convert from ${contentType} to ${preferencesToString(outputPreferences)
}, only from ${Object.keys(inputTypes)} to ${Object.keys(outputTypes)}.`, }, only to ${preferencesToString(outputTypes)}.`,
); );
} }
} }

View File

@ -3,12 +3,16 @@ import type { ValuePreference, ValuePreferences } from '../../http/representatio
import { getLoggerFor } from '../../logging/LogUtil'; import { getLoggerFor } from '../../logging/LogUtil';
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { cleanPreferences, getBestPreference, getTypeWeight } from './ConversionUtil'; import { cleanPreferences, getBestPreference, getTypeWeight, preferencesToString } from './ConversionUtil';
import type { RepresentationConverterArgs } from './RepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter';
import { RepresentationConverter } from './RepresentationConverter'; import { RepresentationConverter } from './RepresentationConverter';
import type { TypedRepresentationConverter } from './TypedRepresentationConverter'; import type { TypedRepresentationConverter } from './TypedRepresentationConverter';
type ConverterPreference = ValuePreference & { converter: TypedRepresentationConverter }; type ConverterPreference = {
converter: TypedRepresentationConverter;
inType: string;
outTypes: ValuePreferences;
};
/** /**
* A chain of converters that can go from `inTypes` to `outTypes`. * A chain of converters that can go from `inTypes` to `outTypes`.
@ -17,16 +21,15 @@ type ConverterPreference = ValuePreference & { converter: TypedRepresentationCon
type ConversionPath = { type ConversionPath = {
converters: TypedRepresentationConverter[]; converters: TypedRepresentationConverter[];
intermediateTypes: string[]; intermediateTypes: string[];
inTypes: ValuePreferences; inType: string;
outTypes: ValuePreferences; outTypes: ValuePreferences;
}; };
/** /**
* The result of applying a `ConversionPath` to a specific input. * The result of choosing a specific output for a `ConversionPath`.
*/ */
type MatchedPath = { type MatchedPath = {
path: ConversionPath; path: ConversionPath;
inType: string;
outType: string; outType: string;
weight: number; weight: number;
}; };
@ -78,8 +81,8 @@ export class ChainedConverter extends RepresentationConverter {
return input.representation; return input.representation;
} }
const { path, inType, outType } = match; const { path, outType } = match;
this.logger.debug(`Converting ${inType} -> ${[ ...path.intermediateTypes, outType ].join(' -> ')}.`); this.logger.debug(`Converting ${path.inType} -> ${[ ...path.intermediateTypes, outType ].join(' -> ')}.`);
const args = { ...input }; const args = { ...input };
for (let i = 0; i < path.converters.length - 1; ++i) { for (let i = 0; i < path.converters.length - 1; ++i) {
@ -105,7 +108,7 @@ export class ChainedConverter extends RepresentationConverter {
const weight = getTypeWeight(type, preferences); const weight = getTypeWeight(type, preferences);
if (weight > 0) { if (weight > 0) {
this.logger.debug(`No conversion required: ${type} already matches ${Object.keys(preferences)}`); this.logger.debug(`No conversion required: ${type} already matches ${preferencesToString(preferences)}`);
return { value: type, weight }; return { value: type, weight };
} }
@ -122,13 +125,13 @@ export class ChainedConverter extends RepresentationConverter {
// Generate paths from all converters that match the input type // Generate paths from all converters that match the input type
let paths = await this.converters.reduce(async(matches: Promise<ConversionPath[]>, converter): let paths = await this.converters.reduce(async(matches: Promise<ConversionPath[]>, converter):
Promise<ConversionPath[]> => { Promise<ConversionPath[]> => {
const inTypes = await converter.getInputTypes(); const outTypes = await converter.getOutputTypes(inType);
if (getTypeWeight(inType, inTypes) > 0) { if (Object.keys(outTypes).length > 0) {
(await matches).push({ (await matches).push({
converters: [ converter ], converters: [ converter ],
intermediateTypes: [], intermediateTypes: [],
inTypes, inType,
outTypes: await converter.getOutputTypes(), outTypes,
}); });
} }
return matches; return matches;
@ -137,18 +140,18 @@ export class ChainedConverter extends RepresentationConverter {
// It's impossible for a path to have a higher weight than this value // It's impossible for a path to have a higher weight than this value
const maxWeight = Math.max(...Object.values(outPreferences)); const maxWeight = Math.max(...Object.values(outPreferences));
let bestPath = this.findBest(inType, outPreferences, paths); let bestPath = this.findBest(outPreferences, paths);
paths = this.removeBadPaths(paths, maxWeight, inType, bestPath); paths = this.removeBadPaths(paths, maxWeight, bestPath);
// This will always stop at some point since paths can't have the same converter twice // This will always stop at some point since paths can't have the same converter twice
while (paths.length > 0) { while (paths.length > 0) {
// For every path, find all the paths that can be made by adding 1 more converter // For every path, find all the paths that can be made by adding 1 more converter
const promises = paths.map(async(path): Promise<ConversionPath[]> => this.takeStep(path)); const promises = paths.map(async(path): Promise<ConversionPath[]> => this.takeStep(path));
paths = (await Promise.all(promises)).flat(); paths = (await Promise.all(promises)).flat();
const newBest = this.findBest(inType, outPreferences, paths); const newBest = this.findBest(outPreferences, paths);
if (newBest && (!bestPath || newBest.weight > bestPath.weight)) { if (newBest && (!bestPath || newBest.weight > bestPath.weight)) {
bestPath = newBest; bestPath = newBest;
} }
paths = this.removeBadPaths(paths, maxWeight, inType, bestPath); paths = this.removeBadPaths(paths, maxWeight, bestPath);
} }
if (!bestPath) { if (!bestPath) {
@ -161,18 +164,17 @@ export class ChainedConverter extends RepresentationConverter {
} }
/** /**
* Finds the path from the given list that can convert the given type to the given preferences. * Finds the path from the given list that can convert to the given preferences.
* If there are multiple matches the one with the highest result weight gets chosen. * If there are multiple matches the one with the highest result weight gets chosen.
* Will return undefined if there are no matches. * Will return undefined if there are no matches.
*/ */
private findBest(type: string, preferences: ValuePreferences, paths: ConversionPath[]): MatchedPath | undefined { private findBest(preferences: ValuePreferences, paths: ConversionPath[]): MatchedPath | undefined {
// Need to use null instead of undefined so `reduce` doesn't take the first element of the array as `best` // Need to use null instead of undefined so `reduce` doesn't take the first element of the array as `best`
return paths.reduce((best: MatchedPath | null, path): MatchedPath | null => { return paths.reduce((best: MatchedPath | null, path): MatchedPath | null => {
const outMatch = getBestPreference(path.outTypes, preferences); const outMatch = getBestPreference(path.outTypes, preferences);
if (outMatch && !(best && best.weight >= outMatch.weight)) { if (outMatch && !(best && best.weight >= outMatch.weight)) {
// Create new MatchedPath, using the output match above // Create new MatchedPath, using the output match above
const inWeight = getTypeWeight(type, path.inTypes); return { path, outType: outMatch.value, weight: outMatch.weight };
return { path, inType: type, outType: outMatch.value, weight: inWeight * outMatch.weight };
} }
return best; return best;
}, null) ?? undefined; }, null) ?? undefined;
@ -184,11 +186,9 @@ export class ChainedConverter extends RepresentationConverter {
* *
* @param paths - Paths to filter. * @param paths - Paths to filter.
* @param maxWeight - The maximum weight in the output preferences. * @param maxWeight - The maximum weight in the output preferences.
* @param inType - The input type.
* @param bestMatch - The current best path. * @param bestMatch - The current best path.
*/ */
private removeBadPaths(paths: ConversionPath[], maxWeight: number, inType: string, bestMatch?: MatchedPath): private removeBadPaths(paths: ConversionPath[], maxWeight: number, bestMatch?: MatchedPath): ConversionPath[] {
ConversionPath[] {
// All paths are still good if there is no best match yet // All paths are still good if there is no best match yet
if (!bestMatch) { if (!bestMatch) {
return paths; return paths;
@ -200,9 +200,7 @@ export class ChainedConverter extends RepresentationConverter {
// Only return paths that can potentially improve upon bestPath // Only return paths that can potentially improve upon bestPath
return paths.filter((path): boolean => { return paths.filter((path): boolean => {
const optimisticWeight = getTypeWeight(inType, path.inTypes) * const optimisticWeight = Math.max(...Object.values(path.outTypes)) * maxWeight;
Math.max(...Object.values(path.outTypes)) *
maxWeight;
return optimisticWeight > bestMatch.weight; return optimisticWeight > bestMatch.weight;
}); });
} }
@ -218,9 +216,9 @@ export class ChainedConverter extends RepresentationConverter {
// Create a new path for every converter that can be appended // Create a new path for every converter that can be appended
return Promise.all(nextConverters.map(async(pref): Promise<ConversionPath> => ({ return Promise.all(nextConverters.map(async(pref): Promise<ConversionPath> => ({
converters: [ ...path.converters, pref.converter ], converters: [ ...path.converters, pref.converter ],
intermediateTypes: [ ...path.intermediateTypes, pref.value ], intermediateTypes: [ ...path.intermediateTypes, pref.inType ],
inTypes: path.inTypes, inType: path.inType,
outTypes: this.modifyTypeWeights(pref.weight, await pref.converter.getOutputTypes()), outTypes: pref.outTypes,
}))); })));
} }
@ -237,13 +235,15 @@ export class ChainedConverter extends RepresentationConverter {
*/ */
private async supportedConverters(types: ValuePreferences, converters: TypedRepresentationConverter[]): private async supportedConverters(types: ValuePreferences, converters: TypedRepresentationConverter[]):
Promise<ConverterPreference[]> { Promise<ConverterPreference[]> {
const promises = converters.map(async(converter): Promise<ConverterPreference | undefined> => { const typeEntries = Object.entries(types);
const inputTypes = await converter.getInputTypes(); const results: ConverterPreference[] = [];
const match = getBestPreference(types, inputTypes); for (const converter of converters) {
if (match) { for (const [ inType, weight ] of typeEntries) {
return { ...match, converter }; let outTypes = await converter.getOutputTypes(inType);
outTypes = this.modifyTypeWeights(weight, outTypes);
results.push({ converter, inType, outTypes });
} }
}); }
return (await Promise.all(promises)).filter(Boolean) as ConverterPreference[]; return results;
} }
} }

View File

@ -164,3 +164,13 @@ export function matchesMediaType(mediaA: string, mediaB: string): boolean {
export function isInternalContentType(contentType?: string): boolean { export function isInternalContentType(contentType?: string): boolean {
return typeof contentType !== 'undefined' && matchesMediaType(contentType, INTERNAL_ALL); return typeof contentType !== 'undefined' && matchesMediaType(contentType, INTERNAL_ALL);
} }
/**
* Serializes a preferences object to a string for display purposes.
* @param preferences - Preferences to serialize
*/
export function preferencesToString(preferences: ValuePreferences): string {
return Object.entries(preferences)
.map(([ type, weight ]): string => `${type}:${weight}`)
.join(',');
}

View File

@ -43,7 +43,7 @@ export class ErrorToTemplateConverter extends BaseTypedRepresentationConverter {
private readonly contentType: string; private readonly contentType: string;
public constructor(templateEngine: TemplateEngine, templateOptions?: TemplateOptions) { public constructor(templateEngine: TemplateEngine, templateOptions?: TemplateOptions) {
super(INTERNAL_ERROR, templateOptions?.contentType ?? DEFAULT_TEMPLATE_OPTIONS.contentType); super(INTERNAL_ERROR, templateOptions?.contentType ?? DEFAULT_TEMPLATE_OPTIONS.contentType!);
// Workaround for https://github.com/LinkedSoftwareDependencies/Components.js/issues/20 // Workaround for https://github.com/LinkedSoftwareDependencies/Components.js/issues/20
if (!templateOptions || Object.keys(templateOptions).length === 0) { if (!templateOptions || Object.keys(templateOptions).length === 0) {
templateOptions = DEFAULT_TEMPLATE_OPTIONS; templateOptions = DEFAULT_TEMPLATE_OPTIONS;

View File

@ -27,7 +27,7 @@ export class QuadToRdfConverter extends BaseTypedRepresentationConverter {
public async handle({ identifier, representation: quads, preferences }: RepresentationConverterArgs): public async handle({ identifier, representation: quads, preferences }: RepresentationConverterArgs):
Promise<Representation> { Promise<Representation> {
// Can not be undefined if the `canHandle` call passed // Can not be undefined if the `canHandle` call passed
const contentType = getConversionTarget(await this.getOutputTypes(), preferences.type)!; const contentType = getConversionTarget(await this.getOutputTypes(INTERNAL_QUADS), preferences.type)!;
let data: Readable; let data: Readable;
// Use prefixes if possible (see https://github.com/rubensworks/rdf-serialize.js/issues/1) // Use prefixes if possible (see https://github.com/rubensworks/rdf-serialize.js/issues/1)

View File

@ -6,12 +6,7 @@ import { RepresentationConverter } from './RepresentationConverter';
*/ */
export abstract class TypedRepresentationConverter extends RepresentationConverter { export abstract class TypedRepresentationConverter extends RepresentationConverter {
/** /**
* Gets the supported input content types for this converter, mapped to a numerical priority. * Gets the output content types this converter can convert the input type to, mapped to a numerical priority.
*/ */
public abstract getInputTypes(): Promise<ValuePreferences>; public abstract getOutputTypes(contentType: string): Promise<ValuePreferences>;
/**
* Gets the supported output content types for this converter, mapped to a numerical quality.
*/
public abstract getOutputTypes(): Promise<ValuePreferences>;
} }

View File

@ -22,15 +22,7 @@ const logger: jest.Mocked<Logger> = getLoggerFor('GuardedStream') as any;
class DummyConverter extends BaseTypedRepresentationConverter { class DummyConverter extends BaseTypedRepresentationConverter {
public constructor() { public constructor() {
super('*/*', 'custom/type'); super(INTERNAL_QUADS, 'x/x');
}
public async getInputTypes(): Promise<Record<string, number>> {
return { [INTERNAL_QUADS]: 1 };
}
public async getOutputTypes(): Promise<Record<string, number>> {
return { 'x/x': 1 };
} }
public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> { public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {

View File

@ -7,56 +7,37 @@ class CustomTypedRepresentationConverter extends BaseTypedRepresentationConverte
} }
describe('A BaseTypedRepresentationConverter', (): void => { describe('A BaseTypedRepresentationConverter', (): void => {
it('defaults to allowing everything.', async(): Promise<void> => {
const converter = new CustomTypedRepresentationConverter();
await expect(converter.getInputTypes()).resolves.toEqual({
});
await expect(converter.getOutputTypes()).resolves.toEqual({
});
});
it('accepts strings.', async(): Promise<void> => { it('accepts strings.', async(): Promise<void> => {
const converter = new CustomTypedRepresentationConverter('a/b', 'c/d'); const converter = new CustomTypedRepresentationConverter('a/b', 'c/d');
await expect(converter.getInputTypes()).resolves.toEqual({ await expect(converter.getOutputTypes('a/b')).resolves.toEqual({
'a/b': 1,
});
await expect(converter.getOutputTypes()).resolves.toEqual({
'c/d': 1, 'c/d': 1,
}); });
}); });
it('accepts string arrays.', async(): Promise<void> => { it('accepts string arrays.', async(): Promise<void> => {
const converter = new CustomTypedRepresentationConverter([ 'a/b', 'c/d' ], [ 'e/f', 'g/h' ]); const converter = new CustomTypedRepresentationConverter([ 'a/b', 'c/d' ], [ 'e/f', 'g/h' ]);
await expect(converter.getInputTypes()).resolves.toEqual({ const output = { 'e/f': 1, 'g/h': 1 };
'a/b': 1, await expect(converter.getOutputTypes('a/b')).resolves.toEqual(output);
'c/d': 1, await expect(converter.getOutputTypes('c/d')).resolves.toEqual(output);
});
await expect(converter.getOutputTypes()).resolves.toEqual({
'e/f': 1,
'g/h': 1,
});
}); });
it('accepts records.', async(): Promise<void> => { it('accepts records.', async(): Promise<void> => {
const converter = new CustomTypedRepresentationConverter({ 'a/b': 0.5 }, { 'c/d': 0.5 }); const converter = new CustomTypedRepresentationConverter({ 'a/b': 0.5 }, { 'c/d': 0.5 });
await expect(converter.getInputTypes()).resolves.toEqual({ await expect(converter.getOutputTypes('a/b')).resolves.toEqual({
'a/b': 0.5, 'c/d': 0.5 * 0.5,
});
await expect(converter.getOutputTypes()).resolves.toEqual({
'c/d': 0.5,
}); });
}); });
it('can not handle input without a Content-Type.', async(): Promise<void> => { it('can not handle input without a Content-Type.', async(): Promise<void> => {
const args: RepresentationConverterArgs = { representation: { metadata: { }}, preferences: {}} as any; const args: RepresentationConverterArgs = { representation: { metadata: { }}, preferences: {}} as any;
const converter = new CustomTypedRepresentationConverter('*/*'); const converter = new CustomTypedRepresentationConverter('*/*', 'b/b');
await expect(converter.canHandle(args)).rejects.toThrow(NotImplementedHttpError); await expect(converter.canHandle(args)).rejects.toThrow(NotImplementedHttpError);
}); });
it('can not handle a type that does not match the input types.', async(): Promise<void> => { it('can not handle a type that does not match the input types.', async(): Promise<void> => {
const args: RepresentationConverterArgs = const args: RepresentationConverterArgs =
{ representation: { metadata: { contentType: 'b/b' }}, preferences: {}} as any; { representation: { metadata: { contentType: 'b/b' }}, preferences: {}} as any;
const converter = new CustomTypedRepresentationConverter('a/a'); const converter = new CustomTypedRepresentationConverter('a/a', 'b/b');
await expect(converter.canHandle(args)).rejects.toThrow(NotImplementedHttpError); await expect(converter.canHandle(args)).rejects.toThrow(NotImplementedHttpError);
}); });

View File

@ -15,19 +15,11 @@ class DummyConverter extends BaseTypedRepresentationConverter {
private readonly outTypes: ValuePreferences; private readonly outTypes: ValuePreferences;
public constructor(inTypes: ValuePreferences, outTypes: ValuePreferences) { public constructor(inTypes: ValuePreferences, outTypes: ValuePreferences) {
super(); super(inTypes, outTypes);
this.inTypes = inTypes; this.inTypes = inTypes;
this.outTypes = outTypes; this.outTypes = outTypes;
} }
public async getInputTypes(): Promise<ValuePreferences> {
return this.inTypes;
}
public async getOutputTypes(): Promise<ValuePreferences> {
return this.outTypes;
}
public async handle(input: RepresentationConverterArgs): Promise<Representation> { public async handle(input: RepresentationConverterArgs): Promise<Representation> {
// Make sure the input type is supported // Make sure the input type is supported
const inType = input.representation.metadata.contentType!; const inType = input.representation.metadata.contentType!;

View File

@ -6,7 +6,7 @@ import {
getTypeWeight, getTypeWeight,
getWeightedPreferences, isInternalContentType, getWeightedPreferences, isInternalContentType,
matchesMediaPreferences, matchesMediaPreferences,
matchesMediaType, matchesMediaType, preferencesToString,
} from '../../../../src/storage/conversion/ConversionUtil'; } from '../../../../src/storage/conversion/ConversionUtil';
import { InternalServerError } from '../../../../src/util/errors/InternalServerError'; import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
@ -153,4 +153,11 @@ describe('ConversionUtil', (): void => {
expect(isInternalContentType('text/turtle')).toBeFalsy(); expect(isInternalContentType('text/turtle')).toBeFalsy();
}); });
}); });
describe('#preferencesToString', (): void => {
it('returns a string serialization.', async(): Promise<void> => {
const preferences: ValuePreferences = { 'a/*': 1, 'b/b': 0.8, 'c/c': 0 };
expect(preferencesToString(preferences)).toEqual('a/*:1,b/b:0.8,c/c:0');
});
});
}); });

View File

@ -9,8 +9,7 @@ describe('An ErrorToJsonConverter', (): void => {
const preferences = {}; const preferences = {};
it('supports going from errors to json.', async(): Promise<void> => { it('supports going from errors to json.', async(): Promise<void> => {
await expect(converter.getInputTypes()).resolves.toEqual({ 'internal/error': 1 }); await expect(converter.getOutputTypes('internal/error')).resolves.toEqual({ 'application/json': 1 });
await expect(converter.getOutputTypes()).resolves.toEqual({ 'application/json': 1 });
}); });
it('adds all HttpError fields.', async(): Promise<void> => { it('adds all HttpError fields.', async(): Promise<void> => {

View File

@ -13,8 +13,7 @@ describe('An ErrorToQuadConverter', (): void => {
const preferences = {}; const preferences = {};
it('supports going from errors to quads.', async(): Promise<void> => { it('supports going from errors to quads.', async(): Promise<void> => {
await expect(converter.getInputTypes()).resolves.toEqual({ 'internal/error': 1 }); await expect(converter.getOutputTypes('internal/error')).resolves.toEqual({ 'internal/quads': 1 });
await expect(converter.getOutputTypes()).resolves.toEqual({ 'internal/quads': 1 });
}); });
it('adds triples for all error fields.', async(): Promise<void> => { it('adds triples for all error fields.', async(): Promise<void> => {

View File

@ -24,8 +24,7 @@ describe('An ErrorToTemplateConverter', (): void => {
}); });
it('supports going from errors to the given content type.', async(): Promise<void> => { it('supports going from errors to the given content type.', async(): Promise<void> => {
await expect(converter.getInputTypes()).resolves.toEqual({ 'internal/error': 1 }); await expect(converter.getOutputTypes('internal/error')).resolves.toEqual({ 'text/html': 1 });
await expect(converter.getOutputTypes()).resolves.toEqual({ 'text/html': 1 });
}); });
it('works with non-HTTP errors.', async(): Promise<void> => { it('works with non-HTTP errors.', async(): Promise<void> => {

View File

@ -8,8 +8,8 @@ describe('A FormToJsonConverter', (): void => {
const converter = new FormToJsonConverter(); const converter = new FormToJsonConverter();
it('supports going from form data to json.', async(): Promise<void> => { it('supports going from form data to json.', async(): Promise<void> => {
await expect(converter.getInputTypes()).resolves.toEqual({ 'application/x-www-form-urlencoded': 1 }); await expect(converter.getOutputTypes('application/x-www-form-urlencoded'))
await expect(converter.getOutputTypes()).resolves.toEqual({ 'application/json': 1 }); .resolves.toEqual({ 'application/json': 1 });
}); });
it('converts form data to JSON.', async(): Promise<void> => { it('converts form data to JSON.', async(): Promise<void> => {

View File

@ -17,8 +17,7 @@ describe('A MarkdownToHtmlConverter', (): void => {
}); });
it('supports going from markdown to html.', async(): Promise<void> => { it('supports going from markdown to html.', async(): Promise<void> => {
await expect(converter.getInputTypes()).resolves.toEqual({ 'text/markdown': 1 }); await expect(converter.getOutputTypes('text/markdown')).resolves.toEqual({ 'text/html': 1 });
await expect(converter.getOutputTypes()).resolves.toEqual({ 'text/html': 1 });
}); });
it('converts markdown and inserts it in the template.', async(): Promise<void> => { it('converts markdown and inserts it in the template.', async(): Promise<void> => {

View File

@ -19,19 +19,14 @@ describe('A QuadToRdfConverter', (): void => {
metadata = new RepresentationMetadata(identifier, INTERNAL_QUADS); metadata = new RepresentationMetadata(identifier, INTERNAL_QUADS);
}); });
it('supports parsing quads.', async(): Promise<void> => {
await expect(new QuadToRdfConverter().getInputTypes())
.resolves.toEqual({ [INTERNAL_QUADS]: 1 });
});
it('defaults to rdfSerializer preferences when given no output preferences.', async(): Promise<void> => { it('defaults to rdfSerializer preferences when given no output preferences.', async(): Promise<void> => {
await expect(new QuadToRdfConverter().getOutputTypes()) await expect(new QuadToRdfConverter().getOutputTypes(INTERNAL_QUADS))
.resolves.toEqual(await rdfSerializer.getContentTypesPrioritized()); .resolves.toEqual(await rdfSerializer.getContentTypesPrioritized());
}); });
it('supports overriding output preferences.', async(): Promise<void> => { it('supports overriding output preferences.', async(): Promise<void> => {
const outputPreferences = { 'text/turtle': 1 }; const outputPreferences = { 'text/turtle': 1 };
await expect(new QuadToRdfConverter({ outputPreferences }).getOutputTypes()) await expect(new QuadToRdfConverter({ outputPreferences }).getOutputTypes(INTERNAL_QUADS))
.resolves.toEqual(outputPreferences); .resolves.toEqual(outputPreferences);
}); });

View File

@ -16,12 +16,11 @@ describe('A RdfToQuadConverter', (): void => {
const converter = new RdfToQuadConverter(); const converter = new RdfToQuadConverter();
const identifier: ResourceIdentifier = { path: 'path' }; const identifier: ResourceIdentifier = { path: 'path' };
it('supports parsing the same types as rdfParser.', async(): Promise<void> => {
await expect(converter.getInputTypes()).resolves.toEqual(await rdfParser.getContentTypesPrioritized());
});
it('supports serializing as quads.', async(): Promise<void> => { it('supports serializing as quads.', async(): Promise<void> => {
await expect(converter.getOutputTypes()).resolves.toEqual({ [INTERNAL_QUADS]: 1 }); const types = Object.entries(await rdfParser.getContentTypesPrioritized());
for (const [ type, weight ] of types) {
await expect(converter.getOutputTypes(type)).resolves.toEqual({ [INTERNAL_QUADS]: weight });
}
}); });
it('can handle turtle to quad conversions.', async(): Promise<void> => { it('can handle turtle to quad conversions.', async(): Promise<void> => {