diff --git a/src/index.ts b/src/index.ts index 0d9de67da..9d621fb69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -130,6 +130,7 @@ export * from './storage/accessors/SparqlDataAccessor'; // Storage/Conversion export * from './storage/conversion/ChainedConverter'; +export * from './storage/conversion/ConversionUtil'; export * from './storage/conversion/QuadToRdfConverter'; export * from './storage/conversion/RdfToQuadConverter'; export * from './storage/conversion/RepresentationConverter'; diff --git a/src/storage/RepresentationConvertingStore.ts b/src/storage/RepresentationConvertingStore.ts index 7cb26710c..125e7aa2b 100644 --- a/src/storage/RepresentationConvertingStore.ts +++ b/src/storage/RepresentationConvertingStore.ts @@ -4,7 +4,7 @@ import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifie import { getLoggerFor } from '../logging/LogUtil'; import { InternalServerError } from '../util/errors/InternalServerError'; import type { Conditions } from './Conditions'; -import { matchingTypes } from './conversion/ConversionUtil'; +import { matchingMediaTypes } from './conversion/ConversionUtil'; import type { RepresentationConverter, RepresentationConverterArgs } from './conversion/RepresentationConverter'; import { PassthroughStore } from './PassthroughStore'; import type { ResourceStore } from './ResourceStore'; @@ -77,7 +77,7 @@ export class RepresentationConvertingStore 0; + return matchingMediaTypes(preferences, [ contentType ]).length > 0; } /** diff --git a/src/storage/conversion/ChainedConverter.ts b/src/storage/conversion/ChainedConverter.ts index 4294a3a90..5dea6890c 100644 --- a/src/storage/conversion/ChainedConverter.ts +++ b/src/storage/conversion/ChainedConverter.ts @@ -1,6 +1,6 @@ import type { Representation } from '../../ldp/representation/Representation'; import { getLoggerFor } from '../../logging/LogUtil'; -import { validateRequestArgs, matchingMediaType } from './ConversionUtil'; +import { supportsConversion, matchesMediaType } from './ConversionUtil'; import type { RepresentationConverterArgs } from './RepresentationConverter'; import { TypedRepresentationConverter } from './TypedRepresentationConverter'; @@ -47,7 +47,7 @@ export class ChainedConverter extends TypedRepresentationConverter { // 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()); - validateRequestArgs(input, inTypes, outTypes); + supportsConversion(input, inTypes, outTypes); } private filterTypes(typeVals: Record): string[] { @@ -85,7 +85,7 @@ export class ChainedConverter extends TypedRepresentationConverter { for (const rightType of rightKeys) { const rightWeight = rightTypes[rightType]; const weight = leftWeight * rightWeight; - if (weight > bestMatch.weight && matchingMediaType(leftType, rightType)) { + if (weight > bestMatch.weight && matchesMediaType(leftType, rightType)) { bestMatch = { type: leftType, weight }; if (weight === 1) { this.logger.info(`${bestMatch.type} is an exact match between ${leftKeys} and ${rightKeys}`); diff --git a/src/storage/conversion/ConversionUtil.ts b/src/storage/conversion/ConversionUtil.ts index 592771fbb..0174d8aba 100644 --- a/src/storage/conversion/ConversionUtil.ts +++ b/src/storage/conversion/ConversionUtil.ts @@ -21,7 +21,7 @@ import type { RepresentationConverterArgs } from './RepresentationConverter'; * * @returns The weighted and filtered list of matching types. */ -export const matchingTypes = (preferences: RepresentationPreferences, types: string[]): +export const matchingMediaTypes = (preferences: RepresentationPreferences, types: string[]): RepresentationPreference[] => { if (!Array.isArray(preferences.type)) { throw new BadRequestHttpError('Output type required for conversion.'); @@ -65,7 +65,7 @@ RepresentationPreference[] => { * * @returns True if the media type patterns can match each other. */ -export const matchingMediaType = (mediaA: string, mediaB: string): boolean => { +export const matchesMediaType = (mediaA: string, mediaB: string): boolean => { if (mediaA === mediaB) { return true; } @@ -85,24 +85,26 @@ export const matchingMediaType = (mediaA: string, mediaB: string): boolean => { }; /** - * Runs some standard checks on the input request: + * Determines whether the given conversion request is supported, + * given the available content type conversions: * - Checks if there is a content type for the input. * - Checks if the input type is supported by the parser. * - Checks if the parser can produce one of the preferred output types. + * Throws an error with details if conversion is not possible. * @param request - Incoming arguments. * @param supportedIn - Media types that can be parsed by the converter. * @param supportedOut - Media types that can be produced by the converter. */ -export const validateRequestArgs = (request: RepresentationConverterArgs, supportedIn: string[], +export const supportsConversion = (request: RepresentationConverterArgs, supportedIn: string[], supportedOut: string[]): void => { const inType = request.representation.metadata.contentType; if (!inType) { throw new BadRequestHttpError('Input type required for conversion.'); } - if (!supportedIn.some((type): boolean => matchingMediaType(inType, type))) { + if (!supportedIn.some((type): boolean => matchesMediaType(inType, type))) { throw new NotImplementedHttpError(`Can only convert from ${supportedIn} to ${supportedOut}.`); } - if (matchingTypes(request.preferences, supportedOut).length <= 0) { + if (matchingMediaTypes(request.preferences, supportedOut).length <= 0) { throw new NotImplementedHttpError(`Can only convert from ${supportedIn} to ${supportedOut}.`); } }; diff --git a/src/storage/conversion/QuadToRdfConverter.ts b/src/storage/conversion/QuadToRdfConverter.ts index 116552c60..63d73890b 100644 --- a/src/storage/conversion/QuadToRdfConverter.ts +++ b/src/storage/conversion/QuadToRdfConverter.ts @@ -6,7 +6,7 @@ import type { RepresentationPreferences } from '../../ldp/representation/Represe import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { guardStream } from '../../util/GuardedStream'; import { CONTENT_TYPE } from '../../util/UriConstants'; -import { validateRequestArgs, matchingTypes } from './ConversionUtil'; +import { supportsConversion, matchingMediaTypes } from './ConversionUtil'; import type { RepresentationConverterArgs } from './RepresentationConverter'; import { TypedRepresentationConverter } from './TypedRepresentationConverter'; @@ -23,7 +23,7 @@ export class QuadToRdfConverter extends TypedRepresentationConverter { } public async canHandle(input: RepresentationConverterArgs): Promise { - validateRequestArgs(input, [ INTERNAL_QUADS ], await rdfSerializer.getContentTypes()); + supportsConversion(input, [ INTERNAL_QUADS ], await rdfSerializer.getContentTypes()); } public async handle(input: RepresentationConverterArgs): Promise { @@ -31,7 +31,7 @@ export class QuadToRdfConverter extends TypedRepresentationConverter { } private async quadsToRdf(quads: Representation, preferences: RepresentationPreferences): Promise { - const contentType = matchingTypes(preferences, await rdfSerializer.getContentTypes())[0].value; + const contentType = matchingMediaTypes(preferences, await rdfSerializer.getContentTypes())[0].value; const metadata = new RepresentationMetadata(quads.metadata, { [CONTENT_TYPE]: contentType }); return { binary: true, diff --git a/src/storage/conversion/RdfToQuadConverter.ts b/src/storage/conversion/RdfToQuadConverter.ts index 80b8daca9..299e1c1d1 100644 --- a/src/storage/conversion/RdfToQuadConverter.ts +++ b/src/storage/conversion/RdfToQuadConverter.ts @@ -6,7 +6,7 @@ import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { pipeSafely } from '../../util/StreamUtil'; import { CONTENT_TYPE } from '../../util/UriConstants'; -import { validateRequestArgs } from './ConversionUtil'; +import { supportsConversion } from './ConversionUtil'; import type { RepresentationConverterArgs } from './RepresentationConverter'; import { TypedRepresentationConverter } from './TypedRepresentationConverter'; @@ -23,7 +23,7 @@ export class RdfToQuadConverter extends TypedRepresentationConverter { } public async canHandle(input: RepresentationConverterArgs): Promise { - validateRequestArgs(input, await rdfParser.getContentTypes(), [ INTERNAL_QUADS ]); + supportsConversion(input, await rdfParser.getContentTypes(), [ INTERNAL_QUADS ]); } public async handle(input: RepresentationConverterArgs): Promise { diff --git a/test/unit/storage/conversion/ChainedConverter.test.ts b/test/unit/storage/conversion/ChainedConverter.test.ts index 4b266cff3..4ccae6910 100644 --- a/test/unit/storage/conversion/ChainedConverter.test.ts +++ b/test/unit/storage/conversion/ChainedConverter.test.ts @@ -2,7 +2,7 @@ import type { Representation } from '../../../../src/ldp/representation/Represen import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata'; import type { RepresentationPreferences } from '../../../../src/ldp/representation/RepresentationPreferences'; import { ChainedConverter } from '../../../../src/storage/conversion/ChainedConverter'; -import { validateRequestArgs } from '../../../../src/storage/conversion/ConversionUtil'; +import { supportsConversion } from '../../../../src/storage/conversion/ConversionUtil'; import type { RepresentationConverterArgs } from '../../../../src/storage/conversion/RepresentationConverter'; import { TypedRepresentationConverter } from '../../../../src/storage/conversion/TypedRepresentationConverter'; import { CONTENT_TYPE } from '../../../../src/util/UriConstants'; @@ -26,7 +26,7 @@ class DummyConverter extends TypedRepresentationConverter { } public async canHandle(input: RepresentationConverterArgs): Promise { - validateRequestArgs(input, Object.keys(this.inTypes), Object.keys(this.outTypes)); + supportsConversion(input, Object.keys(this.inTypes), Object.keys(this.outTypes)); } public async handle(input: RepresentationConverterArgs): Promise { diff --git a/test/unit/storage/conversion/ConversionUtil.test.ts b/test/unit/storage/conversion/ConversionUtil.test.ts index 5a4d3746a..e7b33ef77 100644 --- a/test/unit/storage/conversion/ConversionUtil.test.ts +++ b/test/unit/storage/conversion/ConversionUtil.test.ts @@ -3,9 +3,9 @@ import { RepresentationMetadata } from '../../../../src/ldp/representation/Repre import type { RepresentationPreferences } from '../../../../src/ldp/representation/RepresentationPreferences'; import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier'; import { - matchingMediaType, - matchingTypes, - validateRequestArgs, + matchesMediaType, + matchingMediaTypes, + supportsConversion, } from '../../../../src/storage/conversion/ConversionUtil'; import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError'; import { InternalServerError } from '../../../../src/util/errors/InternalServerError'; @@ -20,93 +20,93 @@ describe('ConversionUtil', (): void => { representation = { metadata } as Representation; }); - describe('#validateRequestArgs', (): void => { + describe('#supportsConversion', (): void => { it('requires an input type.', async(): Promise => { const preferences: RepresentationPreferences = {}; - expect((): any => validateRequestArgs({ identifier, representation, preferences }, [ 'a/x' ], [ 'a/x' ])) + expect((): any => supportsConversion({ identifier, representation, preferences }, [ 'a/x' ], [ 'a/x' ])) .toThrow('Input type required for conversion.'); }); it('requires a matching input type.', async(): Promise => { metadata.contentType = 'a/x'; const preferences: RepresentationPreferences = { type: [{ value: 'b/x', weight: 1 }]}; - expect((): any => validateRequestArgs({ identifier, representation, preferences }, [ 'c/x' ], [ 'a/x' ])) + expect((): any => supportsConversion({ identifier, representation, preferences }, [ 'c/x' ], [ 'a/x' ])) .toThrow('Can only convert from c/x to a/x.'); }); it('requires a matching output type.', async(): Promise => { metadata.contentType = 'a/x'; const preferences: RepresentationPreferences = { type: [{ value: 'b/x', weight: 1 }]}; - expect((): any => validateRequestArgs({ identifier, representation, preferences }, [ 'a/x' ], [ 'c/x' ])) + expect((): any => supportsConversion({ identifier, representation, preferences }, [ 'a/x' ], [ 'c/x' ])) .toThrow('Can only convert from a/x to c/x.'); }); it('succeeds with a valid input and output type.', async(): Promise => { metadata.contentType = 'a/x'; const preferences: RepresentationPreferences = { type: [{ value: 'b/x', weight: 1 }]}; - expect(validateRequestArgs({ identifier, representation, preferences }, [ 'a/x' ], [ 'b/x' ])) + expect(supportsConversion({ identifier, representation, preferences }, [ 'a/x' ], [ 'b/x' ])) .toBeUndefined(); }); }); - describe('#matchingTypes', (): void => { + describe('#matchingMediaTypes', (): void => { it('requires type preferences.', async(): Promise => { const preferences: RepresentationPreferences = {}; - expect((): any => matchingTypes(preferences, [ 'a/b' ])) + expect((): any => matchingMediaTypes(preferences, [ 'a/b' ])) .toThrow('Output type required for conversion.'); }); it('returns matching types if weight > 0.', async(): Promise => { const preferences: RepresentationPreferences = { type: [{ value: 'a/x', weight: 1 }, { value: 'b/x', weight: 0.5 }, { value: 'c/x', weight: 0 }]}; - expect(matchingTypes(preferences, [ 'b/x', 'c/x' ])).toEqual([{ value: 'b/x', weight: 0.5 }]); + expect(matchingMediaTypes(preferences, [ 'b/x', 'c/x' ])).toEqual([{ value: 'b/x', weight: 0.5 }]); }); it('errors if there are duplicate preferences.', async(): Promise => { const preferences: RepresentationPreferences = { type: [{ value: 'b/x', weight: 1 }, { value: 'b/x', weight: 0 }]}; - expect((): any => matchingTypes(preferences, [ 'b/x' ])) + expect((): any => matchingMediaTypes(preferences, [ 'b/x' ])) .toThrow(new BadRequestHttpError(`Duplicate type preference found: b/x`)); }); it('errors if there invalid types.', async(): Promise => { const preferences: RepresentationPreferences = { type: [{ value: 'b/x', weight: 1 }]}; - expect((): any => matchingTypes(preferences, [ 'noType' ])) + expect((): any => matchingMediaTypes(preferences, [ 'noType' ])) .toThrow(new InternalServerError(`Unexpected type preference: noType`)); }); it('filters out internal types.', async(): Promise => { const preferences: RepresentationPreferences = { type: [{ value: '*/*', weight: 1 }]}; - expect(matchingTypes(preferences, [ 'a/x', 'internal/quads' ])).toEqual([{ value: 'a/x', weight: 1 }]); + expect(matchingMediaTypes(preferences, [ 'a/x', 'internal/quads' ])).toEqual([{ value: 'a/x', weight: 1 }]); }); it('keeps internal types that are specifically requested.', async(): Promise => { const preferences: RepresentationPreferences = { type: [{ value: '*/*', weight: 1 }, { value: 'internal/*', weight: 0.5 }]}; - expect(matchingTypes(preferences, [ 'a/x', 'internal/quads' ])) + expect(matchingMediaTypes(preferences, [ 'a/x', 'internal/quads' ])) .toEqual([{ value: 'a/x', weight: 1 }, { value: 'internal/quads', weight: 0.5 }]); }); it('takes the most relevant weight for a type.', async(): Promise => { const preferences: RepresentationPreferences = { type: [{ value: '*/*', weight: 1 }, { value: 'internal/quads', weight: 0.5 }]}; - expect(matchingTypes(preferences, [ 'a/x', 'internal/quads' ])) + expect(matchingMediaTypes(preferences, [ 'a/x', 'internal/quads' ])) .toEqual([{ value: 'a/x', weight: 1 }, { value: 'internal/quads', weight: 0.5 }]); }); }); - describe('#matchingMediaType', (): void => { + describe('#matchesMediaType', (): void => { it('matches all possible media types.', async(): Promise => { - expect(matchingMediaType('*/*', 'text/turtle')).toBeTruthy(); - expect(matchingMediaType('text/*', '*/*')).toBeTruthy(); - expect(matchingMediaType('text/*', 'text/turtle')).toBeTruthy(); - expect(matchingMediaType('text/plain', 'text/*')).toBeTruthy(); - expect(matchingMediaType('text/turtle', 'text/turtle')).toBeTruthy(); + expect(matchesMediaType('*/*', 'text/turtle')).toBeTruthy(); + expect(matchesMediaType('text/*', '*/*')).toBeTruthy(); + expect(matchesMediaType('text/*', 'text/turtle')).toBeTruthy(); + expect(matchesMediaType('text/plain', 'text/*')).toBeTruthy(); + expect(matchesMediaType('text/turtle', 'text/turtle')).toBeTruthy(); - expect(matchingMediaType('text/*', 'application/*')).toBeFalsy(); - expect(matchingMediaType('text/plain', 'application/*')).toBeFalsy(); - expect(matchingMediaType('text/plain', 'text/turtle')).toBeFalsy(); + expect(matchesMediaType('text/*', 'application/*')).toBeFalsy(); + expect(matchesMediaType('text/plain', 'application/*')).toBeFalsy(); + expect(matchesMediaType('text/plain', 'text/turtle')).toBeFalsy(); }); }); });