mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Only convert when needed.
This commit is contained in:
parent
a5bc8d22a9
commit
2efebf91fc
@ -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",
|
"@id": "urn:solid-server:default:RdfRepresentationConverter",
|
||||||
"@type": "ChainedConverter",
|
"@type": "ChainedConverter",
|
||||||
@ -54,6 +81,9 @@
|
|||||||
"@id": "urn:solid-server:default:RepresentationConverter",
|
"@id": "urn:solid-server:default:RepresentationConverter",
|
||||||
"@type": "WaterfallHandler",
|
"@type": "WaterfallHandler",
|
||||||
"WaterfallHandler:_handlers": [
|
"WaterfallHandler:_handlers": [
|
||||||
|
{
|
||||||
|
"@id": "urn:solid-server:default:ContentTypeReplacer"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"@id": "urn:solid-server:default:RdfToQuadConverter"
|
"@id": "urn:solid-server:default:RdfToQuadConverter"
|
||||||
},
|
},
|
||||||
|
@ -129,6 +129,7 @@ export * from './storage/accessors/SparqlDataAccessor';
|
|||||||
|
|
||||||
// Storage/Conversion
|
// Storage/Conversion
|
||||||
export * from './storage/conversion/ChainedConverter';
|
export * from './storage/conversion/ChainedConverter';
|
||||||
|
export * from './storage/conversion/ContentTypeReplacer';
|
||||||
export * from './storage/conversion/ConversionUtil';
|
export * from './storage/conversion/ConversionUtil';
|
||||||
export * from './storage/conversion/QuadToRdfConverter';
|
export * from './storage/conversion/QuadToRdfConverter';
|
||||||
export * from './storage/conversion/RdfToQuadConverter';
|
export * from './storage/conversion/RdfToQuadConverter';
|
||||||
|
75
src/storage/conversion/ContentTypeReplacer.ts
Normal file
75
src/storage/conversion/ContentTypeReplacer.ts
Normal file
@ -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
test/unit/storage/conversion/ContentTypeReplacer.test.ts
Normal file
97
test/unit/storage/conversion/ContentTypeReplacer.test.ts
Normal file
@ -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');
|
||||||
|
});
|
||||||
|
});
|
@ -13,7 +13,7 @@ import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
|
|||||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||||
import { CONTENT_TYPE } from '../../../../src/util/Vocabularies';
|
import { CONTENT_TYPE } from '../../../../src/util/Vocabularies';
|
||||||
|
|
||||||
describe('A RdfToQuadConverter.test.ts', (): void => {
|
describe('A RdfToQuadConverter', (): void => {
|
||||||
const converter = new RdfToQuadConverter();
|
const converter = new RdfToQuadConverter();
|
||||||
const identifier: ResourceIdentifier = { path: 'path' };
|
const identifier: ResourceIdentifier = { path: 'path' };
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user