-
Notifications
You must be signed in to change notification settings - Fork 125
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add IfNeededConverter and PassthroughConverter.
- Loading branch information
1 parent
5429014
commit 6763500
Showing
6 changed files
with
213 additions
and
61 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
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,61 @@ | ||
import type { Representation } from '../../ldp/representation/Representation'; | ||
import { getLoggerFor } from '../../logging/LogUtil'; | ||
import { InternalServerError } from '../../util/errors/InternalServerError'; | ||
import { matchingMediaTypes } from './ConversionUtil'; | ||
import { RepresentationConverter } from './RepresentationConverter'; | ||
import type { RepresentationConverterArgs } from './RepresentationConverter'; | ||
|
||
/** | ||
* A {@link RepresentationConverter} that only converts representations | ||
* that are not compatible with the preferences. | ||
*/ | ||
export class IfNeededConverter extends RepresentationConverter { | ||
private readonly converter: RepresentationConverter; | ||
protected readonly logger = getLoggerFor(this); | ||
|
||
public constructor(converter: RepresentationConverter) { | ||
super(); | ||
this.converter = converter; | ||
} | ||
|
||
public async canHandle(args: RepresentationConverterArgs): Promise<void> { | ||
if (this.needsConversion(args)) { | ||
await this.converter.canHandle(args); | ||
} | ||
} | ||
|
||
public async handle(args: RepresentationConverterArgs): Promise<Representation> { | ||
return !this.needsConversion(args) ? args.representation : this.convert(args, false); | ||
} | ||
|
||
public async handleSafe(args: RepresentationConverterArgs): Promise<Representation> { | ||
return !this.needsConversion(args) ? args.representation : this.convert(args, true); | ||
} | ||
|
||
protected needsConversion({ identifier, representation, preferences }: RepresentationConverterArgs): boolean { | ||
// No conversion needed if no preferences were specified | ||
if (!preferences.type) { | ||
return false; | ||
} | ||
|
||
// No conversion is needed if there are any matches for the provided content type | ||
const { contentType } = representation.metadata; | ||
if (!contentType) { | ||
throw new InternalServerError('Content-Type is required for data conversion.'); | ||
} | ||
const noMatchingMediaType = matchingMediaTypes(preferences.type, { [contentType]: 1 }).length === 0; | ||
if (noMatchingMediaType) { | ||
this.logger.debug(`Conversion needed for ${identifier | ||
.path} from ${representation.metadata.contentType} to satisfy ${Object.entries(preferences.type) | ||
.map(([ value, weight ]): string => `${value};q=${weight}`).join(', ')}`); | ||
} | ||
return noMatchingMediaType; | ||
} | ||
|
||
protected async convert(args: RepresentationConverterArgs, safely: boolean): Promise<Representation> { | ||
const converted = await (safely ? this.converter.handleSafe(args) : this.converter.handle(args)); | ||
this.logger.info(`Converted representation for ${args.identifier | ||
.path} from ${args.representation.metadata.contentType} to ${converted.metadata.contentType}`); | ||
return converted; | ||
} | ||
} |
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,12 @@ | ||
import type { Representation } from '../../ldp/representation/Representation'; | ||
import { RepresentationConverter } from './RepresentationConverter'; | ||
import type { RepresentationConverterArgs } from './RepresentationConverter'; | ||
|
||
/** | ||
* A {@link RepresentationConverter} that does not perform any conversion. | ||
*/ | ||
export class PassthroughConverter extends RepresentationConverter { | ||
public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> { | ||
return representation; | ||
} | ||
} |
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,103 @@ | ||
import type { Representation } from '../../../../src/ldp/representation/Representation'; | ||
import { IfNeededConverter } from '../../../../src/storage/conversion/IfNeededConverter'; | ||
import type { | ||
RepresentationConverter, | ||
} from '../../../../src/storage/conversion/RepresentationConverter'; | ||
|
||
describe('An IfNeededConverter', (): void => { | ||
const identifier = { path: 'identifier' }; | ||
const representation: Representation = { | ||
metadata: { contentType: 'text/turtle' }, | ||
} as any; | ||
const converted = { | ||
metadata: { contentType: 'application/ld+json' }, | ||
}; | ||
|
||
const innerConverter: jest.Mocked<RepresentationConverter> = { | ||
canHandle: jest.fn().mockResolvedValue(true), | ||
handle: jest.fn().mockResolvedValue(converted), | ||
handleSafe: jest.fn().mockResolvedValue(converted), | ||
} as any; | ||
|
||
const converter = new IfNeededConverter(innerConverter); | ||
|
||
afterEach((): void => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('performs no conversion when there are no content type preferences.', async(): Promise<void> => { | ||
const preferences = {}; | ||
const args = { identifier, representation, preferences }; | ||
|
||
await expect(converter.canHandle(args)).resolves.toBeUndefined(); | ||
await expect(converter.handle(args)).resolves.toBe(representation); | ||
await expect(converter.handleSafe(args)).resolves.toBe(representation); | ||
|
||
expect(innerConverter.canHandle).toHaveBeenCalledTimes(0); | ||
expect(innerConverter.handle).toHaveBeenCalledTimes(0); | ||
expect(innerConverter.handleSafe).toHaveBeenCalledTimes(0); | ||
}); | ||
|
||
it('errors if no content type is specified on the representation.', async(): Promise<void> => { | ||
const preferences = { type: { 'text/turtle': 1 }}; | ||
const args = { identifier, representation: { metadata: {}} as any, preferences }; | ||
|
||
await expect(converter.handleSafe(args)).rejects | ||
.toThrow('Content-Type is required for data conversion.'); | ||
|
||
expect(innerConverter.canHandle).toHaveBeenCalledTimes(0); | ||
expect(innerConverter.handle).toHaveBeenCalledTimes(0); | ||
expect(innerConverter.handleSafe).toHaveBeenCalledTimes(0); | ||
}); | ||
|
||
it('performs no conversion when the content type matches the preferences.', async(): Promise<void> => { | ||
const preferences = { type: { 'text/turtle': 1 }}; | ||
const args = { identifier, representation, preferences }; | ||
|
||
await expect(converter.handleSafe(args)).resolves.toBe(representation); | ||
|
||
expect(innerConverter.canHandle).toHaveBeenCalledTimes(0); | ||
expect(innerConverter.handle).toHaveBeenCalledTimes(0); | ||
expect(innerConverter.handleSafe).toHaveBeenCalledTimes(0); | ||
}); | ||
|
||
it('performs a conversion when the content type matches the preferences.', async(): Promise<void> => { | ||
const preferences = { type: { 'text/turtle': 0 }}; | ||
const args = { identifier, representation, preferences }; | ||
|
||
await expect(converter.handleSafe(args)).resolves.toBe(converted); | ||
|
||
expect(innerConverter.canHandle).toHaveBeenCalledTimes(0); | ||
expect(innerConverter.handle).toHaveBeenCalledTimes(0); | ||
expect(innerConverter.handleSafe).toHaveBeenCalledTimes(1); | ||
expect(innerConverter.handleSafe).toHaveBeenCalledWith(args); | ||
}); | ||
|
||
it('does not support conversion when the inner converter does not support it.', async(): Promise<void> => { | ||
const preferences = { type: { 'text/turtle': 0 }}; | ||
const args = { identifier, representation, preferences }; | ||
const error = new Error('unsupported'); | ||
innerConverter.canHandle.mockRejectedValueOnce(error); | ||
|
||
await expect(converter.canHandle(args)).rejects.toThrow(error); | ||
|
||
expect(innerConverter.canHandle).toHaveBeenCalledTimes(1); | ||
expect(innerConverter.canHandle).toHaveBeenCalledWith(args); | ||
}); | ||
|
||
it('supports conversion when the inner converter supports it.', async(): Promise<void> => { | ||
const preferences = { type: { 'text/turtle': 0 }}; | ||
const args = { identifier, representation, preferences }; | ||
|
||
await expect(converter.canHandle(args)).resolves.toBeUndefined(); | ||
|
||
expect(innerConverter.canHandle).toHaveBeenCalledTimes(1); | ||
expect(innerConverter.canHandle).toHaveBeenCalledWith(args); | ||
|
||
await expect(converter.handle(args)).resolves.toBe(converted); | ||
|
||
expect(innerConverter.canHandle).toHaveBeenCalledTimes(1); | ||
expect(innerConverter.handle).toHaveBeenCalledTimes(1); | ||
expect(innerConverter.handle).toHaveBeenCalledWith(args); | ||
}); | ||
}); |
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 { PassthroughConverter } from '../../../../src/storage/conversion/PassthroughConverter'; | ||
|
||
describe('A PassthroughConverter', (): void => { | ||
const representation = {}; | ||
const args = { representation } as any; | ||
|
||
const converter = new PassthroughConverter(); | ||
|
||
it('supports any conversion.', async(): Promise<void> => { | ||
await expect(converter.canHandle(args)).resolves.toBeUndefined(); | ||
}); | ||
|
||
it('returns the original representation on handle.', async(): Promise<void> => { | ||
await expect(converter.handle(args)).resolves.toBe(representation); | ||
}); | ||
|
||
it('returns the original representation on handleSafe.', async(): Promise<void> => { | ||
await expect(converter.handleSafe(args)).resolves.toBe(representation); | ||
}); | ||
}); |