mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add ConstantConverter.
This commit is contained in:
parent
a21532ebf8
commit
5416d66a31
@ -131,6 +131,7 @@ export * from './storage/accessors/SparqlDataAccessor';
|
||||
|
||||
// Storage/Conversion
|
||||
export * from './storage/conversion/ChainedConverter';
|
||||
export * from './storage/conversion/ConstantConverter';
|
||||
export * from './storage/conversion/ContentTypeReplacer';
|
||||
export * from './storage/conversion/IfNeededConverter';
|
||||
export * from './storage/conversion/PassthroughConverter';
|
||||
|
58
src/storage/conversion/ConstantConverter.ts
Normal file
58
src/storage/conversion/ConstantConverter.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
|
||||
import type { Representation } from '../../ldp/representation/Representation';
|
||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||
import { hasMatchingMediaTypes, matchesMediaType } from './ConversionUtil';
|
||||
import { RepresentationConverter } from './RepresentationConverter';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
|
||||
/**
|
||||
* A {@link RepresentationConverter} that ensures
|
||||
* a representation for a certain content type is available.
|
||||
*
|
||||
* Representations of the same content type are served as is;
|
||||
* others are replaced by a constant document.
|
||||
*
|
||||
* This can for example be used to serve an index.html file,
|
||||
* which could then interactively load another representation.
|
||||
*/
|
||||
export class ConstantConverter extends RepresentationConverter {
|
||||
private readonly filePath: string;
|
||||
private readonly contentType: string;
|
||||
|
||||
/**
|
||||
* Creates a new constant converter.
|
||||
*
|
||||
* @param filePath - The path to the constant representation.
|
||||
* @param contentType - The content type of the constant representation.
|
||||
*/
|
||||
public constructor(filePath: string, contentType: string) {
|
||||
super();
|
||||
this.filePath = filePath;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public async canHandle({ preferences, representation }: RepresentationConverterArgs): Promise<void> {
|
||||
// Do not replace the representation if there is no preference for our content type
|
||||
if (!preferences.type) {
|
||||
throw new NotImplementedHttpError('No content type preferences specified');
|
||||
}
|
||||
if (!hasMatchingMediaTypes({ ...preferences.type, '*/*': 0 }, { [this.contentType]: 1 })) {
|
||||
throw new NotImplementedHttpError(`No preference for ${this.contentType}`);
|
||||
}
|
||||
|
||||
// Do not replace the representation if it already has our content type
|
||||
if (matchesMediaType(representation.metadata.contentType ?? '', this.contentType)) {
|
||||
throw new NotImplementedHttpError(`Representation is already ${this.contentType}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {
|
||||
// Ignore the original representation
|
||||
representation.data.destroy();
|
||||
|
||||
// Create a new representation from the constant file
|
||||
const data = await fs.readFile(this.filePath, 'utf8');
|
||||
return new BasicRepresentation(data, representation.metadata, this.contentType);
|
||||
}
|
||||
}
|
@ -55,6 +55,21 @@ string[] {
|
||||
.map(([ type ]): string => type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether any available type satisfies the preferences.
|
||||
*
|
||||
* @param preferredTypes - Preferences for output type.
|
||||
* @param availableTypes - Media types to compare to the preferences.
|
||||
*
|
||||
* @throws BadRequestHttpError
|
||||
* If the type preferences are undefined or if there are duplicate preferences.
|
||||
*
|
||||
* @returns Whether there is at least one preference match.
|
||||
*/
|
||||
export function hasMatchingMediaTypes(preferredTypes?: ValuePreferences, availableTypes?: ValuePreferences): boolean {
|
||||
return matchingMediaTypes(preferredTypes, availableTypes).length !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given two media types/ranges match each other.
|
||||
* Takes wildcards into account.
|
||||
|
@ -2,7 +2,7 @@ import type { Representation } from '../../ldp/representation/Representation';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||
import { UnsupportedAsyncHandler } from '../../util/UnsupportedAsyncHandler';
|
||||
import { matchingMediaTypes } from './ConversionUtil';
|
||||
import { hasMatchingMediaTypes } from './ConversionUtil';
|
||||
import { RepresentationConverter } from './RepresentationConverter';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
|
||||
@ -46,7 +46,7 @@ export class IfNeededConverter extends RepresentationConverter {
|
||||
if (!contentType) {
|
||||
throw new InternalServerError('Content-Type is required for data conversion.');
|
||||
}
|
||||
const noMatchingMediaType = matchingMediaTypes(preferences.type, { [contentType]: 1 }).length === 0;
|
||||
const noMatchingMediaType = !hasMatchingMediaTypes(preferences.type, { [contentType]: 1 });
|
||||
if (noMatchingMediaType) {
|
||||
this.logger.debug(`Conversion needed for ${identifier
|
||||
.path} from ${representation.metadata.contentType} to satisfy ${Object.entries(preferences.type)
|
||||
|
67
test/unit/storage/conversion/ConstantConverter.test.ts
Normal file
67
test/unit/storage/conversion/ConstantConverter.test.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import { ConstantConverter } from '../../../../src/storage/conversion/ConstantConverter';
|
||||
|
||||
const readFile = jest.spyOn(fs, 'readFile').mockResolvedValue('file contents');
|
||||
|
||||
describe('A ConstantConverter', (): void => {
|
||||
const identifier = { path: 'identifier' };
|
||||
|
||||
const converter = new ConstantConverter('abc/def/index.html', 'text/html');
|
||||
|
||||
it('does not support requests without content type preferences.', async(): Promise<void> => {
|
||||
const preferences = {};
|
||||
const representation = {} as any;
|
||||
const args = { identifier, representation, preferences };
|
||||
|
||||
await expect(converter.canHandle(args)).rejects
|
||||
.toThrow('No content type preferences specified');
|
||||
});
|
||||
|
||||
it('does not support requests without matching content type preference.', async(): Promise<void> => {
|
||||
const preferences = { type: { 'text/turtle': 1 }};
|
||||
const representation = {} as any;
|
||||
const args = { identifier, representation, preferences };
|
||||
|
||||
await expect(converter.canHandle(args)).rejects
|
||||
.toThrow('No preference for text/html');
|
||||
});
|
||||
|
||||
it('does not support representations that are already in the right format.', async(): Promise<void> => {
|
||||
const preferences = { type: { 'text/html': 1 }};
|
||||
const metadata = new RepresentationMetadata({ contentType: 'text/html' });
|
||||
const representation = { metadata } as any;
|
||||
const args = { identifier, representation, preferences };
|
||||
|
||||
await expect(converter.canHandle(args)).rejects
|
||||
.toThrow('Representation is already text/html');
|
||||
});
|
||||
|
||||
it('supports representations with an unknown content type.', async(): Promise<void> => {
|
||||
const preferences = { type: { 'text/html': 1 }};
|
||||
const metadata = new RepresentationMetadata();
|
||||
const representation = { metadata } as any;
|
||||
const args = { identifier, representation, preferences };
|
||||
|
||||
await expect(converter.canHandle(args)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('replaces the representation of a supported request.', async(): Promise<void> => {
|
||||
const preferences = { type: { 'text/html': 1 }};
|
||||
const metadata = new RepresentationMetadata({ contentType: 'text/turtle' });
|
||||
const representation = { metadata, data: { destroy: jest.fn() }} as any;
|
||||
const args = { identifier, representation, preferences };
|
||||
|
||||
await expect(converter.canHandle(args)).resolves.toBeUndefined();
|
||||
const converted = await converter.handle(args);
|
||||
|
||||
expect(representation.data.destroy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(readFile).toHaveBeenCalledTimes(1);
|
||||
expect(readFile).toHaveBeenCalledWith('abc/def/index.html', 'utf8');
|
||||
|
||||
expect(converted.metadata.contentType).toBe('text/html');
|
||||
expect(await arrayifyStream(converted.data)).toEqual([ 'file contents' ]);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user