From 27a5711ec2463cd39b2ec484c615ba4a40decb5b Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Sat, 9 Jan 2021 00:07:44 +0100 Subject: [PATCH] refactor: Simplify TypedRepresentationConverter contruction. --- .componentsignore | 3 +- src/storage/conversion/ChainedConverter.ts | 11 +---- src/storage/conversion/QuadToRdfConverter.ts | 29 +++--------- src/storage/conversion/RdfToQuadConverter.ts | 17 ++----- .../TypedRepresentationConverter.ts | 34 +++++++++++++- .../conversion/QuadToRdfConverter.test.ts | 10 +--- .../TypedRepresentationConverter.test.ts | 47 +++++++++++++++++++ 7 files changed, 96 insertions(+), 55 deletions(-) create mode 100644 test/unit/storage/conversion/TypedRepresentationConverter.test.ts diff --git a/.componentsignore b/.componentsignore index 4156bdb60..988877fa3 100644 --- a/.componentsignore +++ b/.componentsignore @@ -1,4 +1,5 @@ [ "Error", - "EventEmitter" + "EventEmitter", + "ValuePreferencesArg" ] diff --git a/src/storage/conversion/ChainedConverter.ts b/src/storage/conversion/ChainedConverter.ts index 4337b441e..83d9d05a8 100644 --- a/src/storage/conversion/ChainedConverter.ts +++ b/src/storage/conversion/ChainedConverter.ts @@ -1,5 +1,4 @@ import type { Representation } from '../../ldp/representation/Representation'; -import type { ValuePreferences } from '../../ldp/representation/RepresentationPreferences'; import { getLoggerFor } from '../../logging/LogUtil'; import { matchesMediaType } from './ConversionUtil'; import type { RepresentationConverterArgs } from './RepresentationConverter'; @@ -25,6 +24,8 @@ export class ChainedConverter extends TypedRepresentationConverter { throw new Error('At least 2 converters are required.'); } this.converters = [ ...converters ]; + this.inputTypes = this.first.getInputTypes(); + this.outputTypes = this.last.getOutputTypes(); } protected get first(): TypedRepresentationConverter { @@ -35,14 +36,6 @@ export class ChainedConverter extends TypedRepresentationConverter { return this.converters[this.converters.length - 1]; } - public async getInputTypes(): Promise { - return this.first.getInputTypes(); - } - - public async getOutputTypes(): Promise { - return this.last.getOutputTypes(); - } - public async handle(input: RepresentationConverterArgs): Promise { const args = { ...input }; for (let i = 0; i < this.converters.length - 1; ++i) { diff --git a/src/storage/conversion/QuadToRdfConverter.ts b/src/storage/conversion/QuadToRdfConverter.ts index 595e33f7d..65f2dc221 100644 --- a/src/storage/conversion/QuadToRdfConverter.ts +++ b/src/storage/conversion/QuadToRdfConverter.ts @@ -3,10 +3,7 @@ import { StreamWriter } from 'n3'; import rdfSerializer from 'rdf-serialize'; import type { Representation } from '../../ldp/representation/Representation'; import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; -import type { - ValuePreferences, - RepresentationPreferences, -} from '../../ldp/representation/RepresentationPreferences'; +import type { ValuePreferences } from '../../ldp/representation/RepresentationPreferences'; import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { guardStream } from '../../util/GuardedStream'; import { pipeSafely } from '../../util/StreamUtil'; @@ -22,26 +19,14 @@ export class QuadToRdfConverter extends TypedRepresentationConverter { private readonly outputPreferences?: ValuePreferences; public constructor(options: { outputPreferences?: Record } = {}) { - super(); - if (Object.keys(options.outputPreferences ?? {}).length > 0) { - this.outputPreferences = { ...options.outputPreferences }; - } + super( + INTERNAL_QUADS, + options.outputPreferences ?? rdfSerializer.getContentTypesPrioritized(), + ); } - public async getInputTypes(): Promise { - return { [INTERNAL_QUADS]: 1 }; - } - - public async getOutputTypes(): Promise { - return this.outputPreferences ?? rdfSerializer.getContentTypesPrioritized(); - } - - public async handle(input: RepresentationConverterArgs): Promise { - return this.quadsToRdf(input.representation, input.preferences); - } - - private async quadsToRdf(quads: Representation, { type }: RepresentationPreferences): Promise { - const contentType = matchingMediaTypes(type, await this.getOutputTypes())[0]; + public async handle({ representation: quads, preferences }: RepresentationConverterArgs): Promise { + const contentType = matchingMediaTypes(preferences.type, await this.getOutputTypes())[0]; const metadata = new RepresentationMetadata(quads.metadata, { [CONTENT_TYPE]: contentType }); let data: Readable; diff --git a/src/storage/conversion/RdfToQuadConverter.ts b/src/storage/conversion/RdfToQuadConverter.ts index c5a26e08b..c81f13261 100644 --- a/src/storage/conversion/RdfToQuadConverter.ts +++ b/src/storage/conversion/RdfToQuadConverter.ts @@ -2,7 +2,6 @@ import { PassThrough } from 'stream'; import rdfParser from 'rdf-parse'; import type { Representation } from '../../ldp/representation/Representation'; import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata'; -import type { ValuePreferences } from '../../ldp/representation/RepresentationPreferences'; import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { pipeSafely } from '../../util/StreamUtil'; @@ -14,23 +13,15 @@ import { TypedRepresentationConverter } from './TypedRepresentationConverter'; * Converts most major RDF serializations to `internal/quads`. */ export class RdfToQuadConverter extends TypedRepresentationConverter { - public async getInputTypes(): Promise { - return rdfParser.getContentTypesPrioritized(); + public constructor() { + super(rdfParser.getContentTypesPrioritized(), INTERNAL_QUADS); } - public async getOutputTypes(): Promise { - return { [INTERNAL_QUADS]: 1 }; - } - - public async handle(input: RepresentationConverterArgs): Promise { - return this.rdfToQuads(input.representation, input.identifier.path); - } - - private rdfToQuads(representation: Representation, baseIRI: string): Representation { + public async handle({ representation, identifier }: RepresentationConverterArgs): Promise { const metadata = new RepresentationMetadata(representation.metadata, { [CONTENT_TYPE]: INTERNAL_QUADS }); const rawQuads = rdfParser.parse(representation.data, { contentType: representation.metadata.contentType!, - baseIRI, + baseIRI: identifier.path, }); const pass = new PassThrough({ objectMode: true }); diff --git a/src/storage/conversion/TypedRepresentationConverter.ts b/src/storage/conversion/TypedRepresentationConverter.ts index eb1e33658..3b54a8012 100644 --- a/src/storage/conversion/TypedRepresentationConverter.ts +++ b/src/storage/conversion/TypedRepresentationConverter.ts @@ -3,19 +3,49 @@ import { supportsMediaTypeConversion } from './ConversionUtil'; import { RepresentationConverter } from './RepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter'; +type PromiseOrValue = T | Promise; +type ValuePreferencesArg = + PromiseOrValue | + PromiseOrValue | + PromiseOrValue; + +async function toValuePreferences(arg: ValuePreferencesArg): Promise { + const resolved = await arg; + if (typeof resolved === 'string') { + return { [resolved]: 1 }; + } + if (Array.isArray(resolved)) { + return Object.fromEntries(resolved.map((type): [string, number] => [ type, 1 ])); + } + return resolved; +} + /** * A {@link RepresentationConverter} that allows requesting the supported types. */ export abstract class TypedRepresentationConverter extends RepresentationConverter { + protected inputTypes: Promise; + protected outputTypes: Promise; + + public constructor(inputTypes: ValuePreferencesArg = {}, outputTypes: ValuePreferencesArg = {}) { + super(); + this.inputTypes = toValuePreferences(inputTypes); + this.outputTypes = toValuePreferences(outputTypes); + } + /** * Gets the supported input content types for this converter, mapped to a numerical priority. */ - public abstract getInputTypes(): Promise; + public async getInputTypes(): Promise { + return this.inputTypes; + } /** * Gets the supported output content types for this converter, mapped to a numerical quality. */ - public abstract getOutputTypes(): Promise; + public async getOutputTypes(): Promise { + return this.outputTypes; + } /** * Verifies whether this converter supports the input. diff --git a/test/unit/storage/conversion/QuadToRdfConverter.test.ts b/test/unit/storage/conversion/QuadToRdfConverter.test.ts index 275bc73e3..a32a592e8 100644 --- a/test/unit/storage/conversion/QuadToRdfConverter.test.ts +++ b/test/unit/storage/conversion/QuadToRdfConverter.test.ts @@ -24,18 +24,12 @@ describe('A QuadToRdfConverter', (): void => { .resolves.toEqual({ [INTERNAL_QUADS]: 1 }); }); - it('defaults to rdfSerializer preferences when given no preferences.', async(): Promise => { + it('defaults to rdfSerializer preferences when given no output preferences.', async(): Promise => { await expect(new QuadToRdfConverter().getOutputTypes()) .resolves.toEqual(await rdfSerializer.getContentTypesPrioritized()); }); - it('defaults to rdfSerializer preferences when given empty preferences.', async(): Promise => { - const outputPreferences = {}; - await expect(new QuadToRdfConverter({ outputPreferences }).getOutputTypes()) - .resolves.toEqual(await rdfSerializer.getContentTypesPrioritized()); - }); - - it('returns custom preferences when given non-empty preferences.', async(): Promise => { + it('supports overriding output preferences.', async(): Promise => { const outputPreferences = { 'text/turtle': 1 }; await expect(new QuadToRdfConverter({ outputPreferences }).getOutputTypes()) .resolves.toEqual(outputPreferences); diff --git a/test/unit/storage/conversion/TypedRepresentationConverter.test.ts b/test/unit/storage/conversion/TypedRepresentationConverter.test.ts new file mode 100644 index 000000000..cf648463a --- /dev/null +++ b/test/unit/storage/conversion/TypedRepresentationConverter.test.ts @@ -0,0 +1,47 @@ +import { TypedRepresentationConverter } from '../../../../src/storage/conversion/TypedRepresentationConverter'; + +class CustomTypedRepresentationConverter extends TypedRepresentationConverter { + public handle = jest.fn(); +} + +describe('A TypedRepresentationConverter', (): void => { + it('defaults to allowing everything.', async(): Promise => { + const converter = new CustomTypedRepresentationConverter(); + await expect(converter.getInputTypes()).resolves.toEqual({ + }); + await expect(converter.getOutputTypes()).resolves.toEqual({ + }); + }); + + it('accepts strings.', async(): Promise => { + const converter = new CustomTypedRepresentationConverter('a/b', 'c/d'); + await expect(converter.getInputTypes()).resolves.toEqual({ + 'a/b': 1, + }); + await expect(converter.getOutputTypes()).resolves.toEqual({ + 'c/d': 1, + }); + }); + + it('accepts string arrays.', async(): Promise => { + const converter = new CustomTypedRepresentationConverter([ 'a/b', 'c/d' ], [ 'e/f', 'g/h' ]); + await expect(converter.getInputTypes()).resolves.toEqual({ + 'a/b': 1, + 'c/d': 1, + }); + await expect(converter.getOutputTypes()).resolves.toEqual({ + 'e/f': 1, + 'g/h': 1, + }); + }); + + it('accepts records.', async(): Promise => { + const converter = new CustomTypedRepresentationConverter({ 'a/b': 0.5 }, { 'c/d': 0.5 }); + await expect(converter.getInputTypes()).resolves.toEqual({ + 'a/b': 0.5, + }); + await expect(converter.getOutputTypes()).resolves.toEqual({ + 'c/d': 0.5, + }); + }); +});