From 712a690904e544ebfaea21acdcf7d25256c7c07f Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Wed, 21 Oct 2020 11:09:59 +0200 Subject: [PATCH] feat: Update RepresentationConvertingStore to convert incoming data --- src/storage/RepresentationConvertingStore.ts | 53 ++++++++++++++++--- test/configs/Util.ts | 4 +- .../RepresentationConvertingStore.test.ts | 53 +++++++++++++++---- 3 files changed, 89 insertions(+), 21 deletions(-) diff --git a/src/storage/RepresentationConvertingStore.ts b/src/storage/RepresentationConvertingStore.ts index 688067741..70378928b 100644 --- a/src/storage/RepresentationConvertingStore.ts +++ b/src/storage/RepresentationConvertingStore.ts @@ -9,9 +9,15 @@ import { PassthroughStore } from './PassthroughStore'; import type { ResourceStore } from './ResourceStore'; /** - * Store that overrides the `getRepresentation` function. - * Tries to convert the {@link Representation} it got from the source store - * so it matches one of the given type preferences. + * Store that overrides all functions that take or output a {@link Representation}, + * so `getRepresentation`, `addResource`, and `setRepresentation`. + * + * For incoming representations, they will be converted if an incoming converter and preferences have been set. + * The converted Representation will be passed along. + * + * For outgoing representations, they will be converted if there is an outgoing converter. + * + * Conversions will only happen if required and will not happen if the Representation is already in the correct format. * * In the future this class should take the preferences of the request into account. * Even if there is a match with the output from the store, @@ -20,21 +26,44 @@ import type { ResourceStore } from './ResourceStore'; export class RepresentationConvertingStore extends PassthroughStore { protected readonly logger = getLoggerFor(this); - private readonly converter: RepresentationConverter; + private readonly inConverter?: RepresentationConverter; + private readonly outConverter?: RepresentationConverter; - public constructor(source: T, converter: RepresentationConverter) { + private readonly inPreferences?: RepresentationPreferences; + + public constructor(source: T, options: { + outConverter?: RepresentationConverter; + inConverter?: RepresentationConverter; + inPreferences?: RepresentationPreferences; + }) { super(source); - this.converter = converter; + this.inConverter = options.inConverter; + this.outConverter = options.outConverter; + this.inPreferences = options.inPreferences; } public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences, conditions?: Conditions): Promise { const representation = await super.getRepresentation(identifier, preferences, conditions); - if (this.matchesPreferences(representation, preferences)) { + if (!this.outConverter || this.matchesPreferences(representation, preferences)) { return representation; } this.logger.info(`Convert ${identifier.path} from ${representation.metadata.contentType} to ${preferences.type}`); - return this.converter.handleSafe({ identifier, representation, preferences }); + return this.outConverter.handleSafe({ identifier, representation, preferences }); + } + + public async addResource(container: ResourceIdentifier, representation: Representation, + conditions?: Conditions): Promise { + // We can potentially run into problems here if we convert a turtle document where the base IRI is required, + // since we don't know the resource IRI yet at this point. + representation = await this.convertRepresentation(container, representation); + return this.source.addResource(container, representation, conditions); + } + + public async setRepresentation(identifier: ResourceIdentifier, representation: Representation, + conditions?: Conditions): Promise { + representation = await this.convertRepresentation(identifier, representation); + return this.source.setRepresentation(identifier, representation, conditions); } private matchesPreferences(representation: Representation, preferences: RepresentationPreferences): boolean { @@ -49,4 +78,12 @@ export class RepresentationConvertingStore { + if (!this.inPreferences || !this.inConverter || this.matchesPreferences(representation, this.inPreferences)) { + return representation; + } + return this.inConverter.handleSafe({ identifier, representation, preferences: this.inPreferences }); + } } diff --git a/test/configs/Util.ts b/test/configs/Util.ts index 8d166a8df..cf58a6bc3 100644 --- a/test/configs/Util.ts +++ b/test/configs/Util.ts @@ -76,11 +76,11 @@ export const getInMemoryResourceStore = (base = BASE): DataAccessorBasedStore => */ export const getConvertingStore = (store: ResourceStore, converters: RepresentationConverter[]): RepresentationConvertingStore => - new RepresentationConvertingStore(store, new CompositeAsyncHandler(converters)); + new RepresentationConvertingStore(store, { outConverter: new CompositeAsyncHandler(converters) }); /** * Gives a patching store based on initial store. - * @param store - Inital resource store. + * @param store - Initial resource store. * * @returns The patching store. */ diff --git a/test/unit/storage/RepresentationConvertingStore.test.ts b/test/unit/storage/RepresentationConvertingStore.test.ts index b44fde42e..696effd74 100644 --- a/test/unit/storage/RepresentationConvertingStore.test.ts +++ b/test/unit/storage/RepresentationConvertingStore.test.ts @@ -1,4 +1,6 @@ +import type { Representation } from '../../../src/ldp/representation/Representation'; import { RepresentationMetadata } from '../../../src/ldp/representation/RepresentationMetadata'; +import type { RepresentationPreferences } from '../../../src/ldp/representation/RepresentationPreferences'; import type { RepresentationConverter } from '../../../src/storage/conversion/RepresentationConverter'; import { RepresentationConvertingStore } from '../../../src/storage/RepresentationConvertingStore'; import type { ResourceStore } from '../../../src/storage/ResourceStore'; @@ -7,19 +9,24 @@ import { CONTENT_TYPE } from '../../../src/util/UriConstants'; describe('A RepresentationConvertingStore', (): void => { let store: RepresentationConvertingStore; let source: ResourceStore; - let handleSafeFn: jest.Mock, []>; - let converter: RepresentationConverter; + let inConverter: RepresentationConverter; + let outConverter: RepresentationConverter; + const inPreferences: RepresentationPreferences = { type: [{ value: 'text/turtle', weight: 1 }]}; const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' }); + let representation: Representation; beforeEach(async(): Promise => { source = { getRepresentation: jest.fn(async(): Promise => ({ data: 'data', metadata })), - } as unknown as ResourceStore; + addResource: jest.fn(), + setRepresentation: jest.fn(), + } as any; - handleSafeFn = jest.fn(async(): Promise => 'converter'); - converter = { handleSafe: handleSafeFn } as unknown as RepresentationConverter; + inConverter = { handleSafe: jest.fn(async(): Promise => 'inConvert') } as any; + outConverter = { handleSafe: jest.fn(async(): Promise => 'outConvert') } as any; - store = new RepresentationConvertingStore(source, converter); + store = new RepresentationConvertingStore(source, { inPreferences, inConverter, outConverter }); + representation = { binary: true, data: 'data', metadata } as any; }); it('returns the Representation from the source if no changes are required.', async(): Promise => { @@ -35,7 +42,7 @@ describe('A RepresentationConvertingStore', (): void => { expect(source.getRepresentation).toHaveBeenLastCalledWith( { path: 'path' }, { type: [{ value: 'text/*', weight: 0 }, { value: 'text/turtle', weight: 1 }]}, undefined, ); - expect(handleSafeFn).toHaveBeenCalledTimes(0); + expect(outConverter.handleSafe).toHaveBeenCalledTimes(0); }); it('returns the Representation from the source if there are no preferences.', async(): Promise => { @@ -49,19 +56,43 @@ describe('A RepresentationConvertingStore', (): void => { expect(source.getRepresentation).toHaveBeenLastCalledWith( { path: 'path' }, {}, undefined, ); - expect(handleSafeFn).toHaveBeenCalledTimes(0); + expect(outConverter.handleSafe).toHaveBeenCalledTimes(0); }); it('calls the converter if another output is preferred.', async(): Promise => { await expect(store.getRepresentation({ path: 'path' }, { type: [ { value: 'text/plain', weight: 1 }, { value: 'text/turtle', weight: 0 }, - ]})).resolves.toEqual('converter'); + ]})).resolves.toEqual('outConvert'); expect(source.getRepresentation).toHaveBeenCalledTimes(1); - expect(handleSafeFn).toHaveBeenCalledTimes(1); - expect(handleSafeFn).toHaveBeenLastCalledWith({ + expect(outConverter.handleSafe).toHaveBeenCalledTimes(1); + expect(outConverter.handleSafe).toHaveBeenLastCalledWith({ identifier: { path: 'path' }, representation: { data: 'data', metadata }, preferences: { type: [{ value: 'text/plain', weight: 1 }, { value: 'text/turtle', weight: 0 }]}, }); }); + + it('keeps the representation if the conversion is not required.', async(): Promise => { + const id = { path: 'identifier' }; + + await expect(store.addResource(id, representation, 'conditions' as any)).resolves.toBeUndefined(); + expect(source.addResource).toHaveBeenLastCalledWith(id, representation, 'conditions'); + + await expect(store.setRepresentation(id, representation, 'conditions' as any)).resolves.toBeUndefined(); + expect(inConverter.handleSafe).toHaveBeenCalledTimes(0); + expect(source.setRepresentation).toHaveBeenLastCalledWith(id, representation, 'conditions'); + }); + + it('converts the data if it is required.', async(): Promise => { + metadata.contentType = 'text/plain'; + const id = { path: 'identifier' }; + + await expect(store.addResource(id, representation, 'conditions' as any)).resolves.toBeUndefined(); + expect(inConverter.handleSafe).toHaveBeenCalledTimes(1); + expect(source.addResource).toHaveBeenLastCalledWith(id, 'inConvert', 'conditions'); + + await expect(store.setRepresentation(id, representation, 'conditions' as any)).resolves.toBeUndefined(); + expect(inConverter.handleSafe).toHaveBeenCalledTimes(2); + expect(source.setRepresentation).toHaveBeenLastCalledWith(id, 'inConvert', 'conditions'); + }); });