-
Notifications
You must be signed in to change notification settings - Fork 131
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a5bc8d2
commit 2efebf9
Showing
5 changed files
with
204 additions
and
1 deletion.
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,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]; | ||
} | ||
} |
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,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'); | ||
}); | ||
}); |
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