Skip to content

Commit

Permalink
feat: Only convert when needed.
Browse files Browse the repository at this point in the history
  • Loading branch information
RubenVerborgh committed Jan 7, 2021
1 parent a5bc8d2 commit 2efebf9
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 1 deletion.
30 changes: 30 additions & 0 deletions config/presets/representation-conversion.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,33 @@
]
},

{
"@id": "urn:solid-server:default:ContentTypeReplacer",
"@type": "ContentTypeReplacer",
"ContentTypeReplacer:_replacements": [
{
"ContentTypeReplacer:_replacements_key": "application/n-triples",
"ContentTypeReplacer:_replacements_value": "text/turtle"
},
{
"ContentTypeReplacer:_replacements_key": "text/turtle",
"ContentTypeReplacer:_replacements_value": "application/trig"
},
{
"ContentTypeReplacer:_replacements_key": "application/ld+json",
"ContentTypeReplacer:_replacements_value": "application/json"
},
{
"ContentTypeReplacer:_replacements_key": "application/*",
"ContentTypeReplacer:_replacements_value": "application/octet-stream"
},
{
"ContentTypeReplacer:_replacements_key": "text/*",
"ContentTypeReplacer:_replacements_value": "application/octet-stream"
}
]
},

{
"@id": "urn:solid-server:default:RdfRepresentationConverter",
"@type": "ChainedConverter",
Expand All @@ -54,6 +81,9 @@
"@id": "urn:solid-server:default:RepresentationConverter",
"@type": "WaterfallHandler",
"WaterfallHandler:_handlers": [
{
"@id": "urn:solid-server:default:ContentTypeReplacer"
},
{
"@id": "urn:solid-server:default:RdfToQuadConverter"
},
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export * from './storage/accessors/SparqlDataAccessor';

// Storage/Conversion
export * from './storage/conversion/ChainedConverter';
export * from './storage/conversion/ContentTypeReplacer';
export * from './storage/conversion/ConversionUtil';
export * from './storage/conversion/QuadToRdfConverter';
export * from './storage/conversion/RdfToQuadConverter';
Expand Down
75 changes: 75 additions & 0 deletions src/storage/conversion/ContentTypeReplacer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { Representation } from '../../ldp/representation/Representation';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import type { ValuePreferences } from '../../ldp/representation/RepresentationPreferences';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { CONTENT_TYPE } from '../../util/Vocabularies';
import { matchesMediaType, matchingMediaTypes } from './ConversionUtil';
import type { RepresentationConverterArgs } from './RepresentationConverter';
import { RepresentationConverter } from './RepresentationConverter';

/**
* A {@link RepresentationConverter} that changes the content type
* but does not alter the representation.
*
* Useful for when a content type is binary-compatible with another one;
* for instance, all JSON-LD files are valid JSON files.
*/
export class ContentTypeReplacer extends RepresentationConverter {
private readonly contentTypeMap: Record<string, ValuePreferences> = {};

/**
* @param replacements - Map of content type patterns and content types to replace them by.
*/
public constructor(replacements: Record<string, string>);
public constructor(replacements: Record<string, Iterable<string>>);
public constructor(replacements: Record<string, any>) {
super();
// Store the replacements as value preferences,
// completing any transitive chains (A:B, B:C, C:D => A:B,C,D)
for (const inputType of Object.keys(replacements)) {
this.contentTypeMap[inputType] = {};
(function addReplacements(inType, outTypes): void {
const replace = replacements[inType] ?? [];
const newTypes = typeof replace === 'string' ? [ replace ] : replace;
for (const newType of newTypes) {
if (!(newType in outTypes)) {
outTypes[newType] = 1;
addReplacements(newType, outTypes);
}
}
})(inputType, this.contentTypeMap[inputType]);
}
}

public async canHandle({ representation, preferences }: RepresentationConverterArgs): Promise<void> {
this.getReplacementType(representation.metadata.contentType, preferences.type);
}

/**
* Changes the content type on the representation.
*/
public async handle({ representation, preferences }: RepresentationConverterArgs): Promise<Representation> {
const contentType = this.getReplacementType(representation.metadata.contentType, preferences.type);
const metadata = new RepresentationMetadata(representation.metadata, { [CONTENT_TYPE]: contentType });
return { ...representation, metadata };
}

public async handleSafe(args: RepresentationConverterArgs): Promise<Representation> {
return this.handle(args);
}

/**
* Find a replacement content type that matches the preferences,
* or throws an error if none was found.
*/
private getReplacementType(contentType = 'unknown', preferred: ValuePreferences = {}): string {
const supported = Object.keys(this.contentTypeMap)
.filter((type): boolean => matchesMediaType(contentType, type))
.map((type): ValuePreferences => this.contentTypeMap[type]);
const matching = matchingMediaTypes(preferred, Object.assign({} as ValuePreferences, ...supported));
if (matching.length === 0) {
throw new NotImplementedHttpError(`Cannot convert from ${contentType} to ${Object.keys(preferred)}`);
}
return matching[0];
}
}
97 changes: 97 additions & 0 deletions test/unit/storage/conversion/ContentTypeReplacer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import 'jest-rdf';
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
import { ContentTypeReplacer } from '../../../../src/storage/conversion/ContentTypeReplacer';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';

const binary = true;
const data = { data: true };

describe('A ContentTypeReplacer', (): void => {
const converter = new ContentTypeReplacer({
'application/n-triples': [
'text/turtle',
'application/trig',
'application/n-quads',
],
'application/ld+json': 'application/json',
'application/json': 'application/octet-stream',
'application/octet-stream': 'internal/anything',
'internal/anything': 'application/octet-stream',
'*/*': 'application/octet-stream',
});

it('throws on an unsupported input type.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({ contentType: 'text/plain' });
const representation = { metadata };
const preferences = { type: { 'application/json': 1 }};

await expect(converter.canHandle({ representation, preferences } as any))
.rejects.toThrow(new Error('Cannot convert from text/plain to application/json'));
});

it('throws on an unsupported output type.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({ contentType: 'application/n-triples' });
const representation = { metadata };
const preferences = { type: { 'application/json': 1 }};

await expect(converter.canHandle({ representation, preferences } as any))
.rejects.toThrow(new Error('Cannot convert from application/n-triples to application/json'));
});

it('does not replace when no content type is given.', async(): Promise<void> => {
const metadata = new RepresentationMetadata();
const representation = { binary, data, metadata };
const preferences = { type: { 'application/json': 1 }};

await expect(converter.canHandle({ representation, preferences } as any))
.rejects.toThrow(new NotImplementedHttpError('Cannot convert from unknown to application/json'));
});

it('replaces a supported content type when no preferences are given.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({ contentType: 'application/n-triples' });
const representation = { binary, data, metadata };
const preferences = {};

const result = await converter.handleSafe({ representation, preferences } as any);
expect(result.binary).toBe(binary);
expect(result.data).toBe(data);
expect(result.metadata.contentType).toBe('text/turtle');
});

it('replaces a supported content type when preferences are given.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({ contentType: 'application/n-triples' });
const representation = { binary, data, metadata };
const preferences = { type: { 'application/n-quads': 1 }};

const result = await converter.handleSafe({ representation, preferences } as any);
expect(result.binary).toBe(binary);
expect(result.data).toBe(data);
expect(result.metadata.contentType).toBe('application/n-quads');
});

it('replaces a supported wildcard type.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({ contentType: 'text/plain' });
const representation = { binary, data, metadata };
const preferences = { type: { 'application/octet-stream': 1 }};

const result = await converter.handleSafe({ representation, preferences } as any);
expect(result.binary).toBe(binary);
expect(result.data).toBe(data);
expect(result.metadata.contentType).toBe('application/octet-stream');
});

it('picks the most preferred content type.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({ contentType: 'application/n-triples' });
const representation = { binary, data, metadata };
const preferences = { type: {
'text/turtle': 0.5,
'application/trig': 0.6,
'application/n-quads': 0.4,
}};

const result = await converter.handleSafe({ representation, preferences } as any);
expect(result.binary).toBe(binary);
expect(result.data).toBe(data);
expect(result.metadata.contentType).toBe('application/trig');
});
});
2 changes: 1 addition & 1 deletion test/unit/storage/conversion/RdfToQuadConverter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
import { CONTENT_TYPE } from '../../../../src/util/Vocabularies';

describe('A RdfToQuadConverter.test.ts', (): void => {
describe('A RdfToQuadConverter', (): void => {
const converter = new RdfToQuadConverter();
const identifier: ResourceIdentifier = { path: 'path' };

Expand Down

0 comments on commit 2efebf9

Please sign in to comment.