mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
refactor: Use record for representation preference.
This commit is contained in:
@@ -122,7 +122,7 @@ export class WebAclAuthorizer extends Authorizer {
|
||||
try {
|
||||
const acl = await this.aclManager.getAclDocument(id);
|
||||
this.logger.debug(`Trying to read the ACL document ${acl.path}`);
|
||||
const data = await this.resourceStore.getRepresentation(acl, { type: [{ value: INTERNAL_QUADS, weight: 1 }]});
|
||||
const data = await this.resourceStore.getRepresentation(acl, { type: { [INTERNAL_QUADS]: 1 }});
|
||||
this.logger.info(`Reading ACL statements from ${acl.path}`);
|
||||
|
||||
const resourceId = await this.aclManager.getAclConstrainedResource(id);
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
parseAcceptEncoding,
|
||||
parseAcceptLanguage,
|
||||
} from '../../util/HeaderUtil';
|
||||
import type { RepresentationPreference } from '../representation/RepresentationPreference';
|
||||
import type { RepresentationPreferences } from '../representation/RepresentationPreferences';
|
||||
import { PreferenceParser } from './PreferenceParser';
|
||||
|
||||
@@ -31,13 +30,13 @@ export class AcceptPreferenceParser extends PreferenceParser {
|
||||
(Object.keys(headers) as (keyof RepresentationPreferences)[]).forEach((key): void => {
|
||||
const preferences = this.parseHeader(headers[key]!.func, headers[key]!.val);
|
||||
if (preferences.length > 0) {
|
||||
result[key] = preferences;
|
||||
result[key] = Object.fromEntries(preferences);
|
||||
}
|
||||
});
|
||||
|
||||
// Accept-DateTime is currently specified to simply have a datetime as value
|
||||
if (input.headers['accept-datetime']) {
|
||||
result.datetime = [{ value: input.headers['accept-datetime'] as string, weight: 1 }];
|
||||
result.datetime = { [input.headers['accept-datetime'] as string]: 1 };
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -48,13 +47,10 @@ export class AcceptPreferenceParser extends PreferenceParser {
|
||||
* @param input - Input header string.
|
||||
* @param parseFunction - Function that converts header string to {@link AcceptHeader}.
|
||||
*
|
||||
* @returns A list of {@link RepresentationPreference}. Returns an empty list if input was not defined.
|
||||
* @returns A list of preferences. Returns an empty list if input was not defined.
|
||||
*/
|
||||
private parseHeader(parseFunction: (input: string) => AcceptHeader[], input?: string): RepresentationPreference[] {
|
||||
if (!input) {
|
||||
return [];
|
||||
}
|
||||
return parseFunction(input).map((accept): RepresentationPreference =>
|
||||
({ value: accept.range, weight: accept.weight }));
|
||||
private parseHeader(parseFunction: (input: string) => AcceptHeader[], input?: string): [string, number][] {
|
||||
return (input ? parseFunction(input) : [])
|
||||
.map(({ range, weight }): [string, number] => [ range, weight ]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
/**
|
||||
* Represents a single preference in a request.
|
||||
*/
|
||||
export interface RepresentationPreference {
|
||||
/**
|
||||
* The actual preference value.
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* How preferred this value is in a number going from 0 to 1.
|
||||
* Follows the quality values rule from RFC 7231:
|
||||
*
|
||||
* "The weight is normalized to a real number in the range 0 through 1,
|
||||
* where 0.001 is the least preferred and 1 is the most preferred; a
|
||||
* value of 0 means "not acceptable"."
|
||||
*/
|
||||
weight: number;
|
||||
}
|
||||
* Represents preferred values along a single content negotiation dimension.
|
||||
*
|
||||
* The number represents how preferred this value is from 0 to 1.
|
||||
* Follows the quality values rule from RFC 7231:
|
||||
* "The weight is normalized to a real number in the range 0 through 1,
|
||||
* where 0.001 is the least preferred and 1 is the most preferred; a
|
||||
* value of 0 means "not acceptable"."
|
||||
*/
|
||||
export type RepresentationPreference = Record<string, number>;
|
||||
|
||||
@@ -4,9 +4,9 @@ import type { RepresentationPreference } from './RepresentationPreference';
|
||||
* Contains the preferences of which kind of representation is requested.
|
||||
*/
|
||||
export interface RepresentationPreferences {
|
||||
type?: RepresentationPreference[];
|
||||
charset?: RepresentationPreference[];
|
||||
datetime?: RepresentationPreference[];
|
||||
encoding?: RepresentationPreference[];
|
||||
language?: RepresentationPreference[];
|
||||
type?: RepresentationPreference;
|
||||
charset?: RepresentationPreference;
|
||||
datetime?: RepresentationPreference;
|
||||
encoding?: RepresentationPreference;
|
||||
language?: RepresentationPreference;
|
||||
}
|
||||
|
||||
@@ -90,8 +90,8 @@ export class RepresentationConvertingStore<T extends ResourceStore = ResourceSto
|
||||
return input.representation;
|
||||
}
|
||||
this.logger.debug(`Conversion needed for ${input.identifier
|
||||
.path} from ${input.representation.metadata.contentType} to satisfy ${input.preferences.type
|
||||
.map((pref): string => `${pref.value};q=${pref.weight}`).join(', ')}`);
|
||||
.path} from ${input.representation.metadata.contentType} to satisfy ${Object.entries(input.preferences.type)
|
||||
.map(([ value, weight ]): string => `${value};q=${weight}`).join(', ')}`);
|
||||
|
||||
const converted = await converter.handleSafe(input);
|
||||
this.logger.info(`Converted representation for ${input.identifier
|
||||
@@ -107,7 +107,7 @@ export class RepresentationConvertingStore<T extends ResourceStore = ResourceSto
|
||||
if (!this.inType) {
|
||||
return representation;
|
||||
}
|
||||
const preferences: RepresentationPreferences = { type: [{ value: this.inType, weight: 1 }]};
|
||||
const preferences: RepresentationPreferences = { type: { [this.inType]: 1 }};
|
||||
|
||||
return this.convertRepresentation({ identifier, representation, preferences }, this.inConverter);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export class ChainedConverter extends TypedRepresentationConverter {
|
||||
const args = { ...input };
|
||||
for (let i = 0; i < this.converters.length - 1; ++i) {
|
||||
const value = await this.getMatchingType(this.converters[i], this.converters[i + 1]);
|
||||
args.preferences = { type: [{ value, weight: 1 }]};
|
||||
args.preferences = { type: { [value]: 1 }};
|
||||
args.representation = await this.converters[i].handle(args);
|
||||
}
|
||||
args.preferences = input.preferences;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { RepresentationPreference } from '../../ldp/representation/RepresentationPreference';
|
||||
import type { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences';
|
||||
import { INTERNAL_ALL } from '../../util/ContentTypes';
|
||||
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||
@@ -21,43 +20,42 @@ import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
*
|
||||
* @returns The weighted and filtered list of matching types.
|
||||
*/
|
||||
export const matchingMediaTypes = (preferences: RepresentationPreferences, types: string[]):
|
||||
RepresentationPreference[] => {
|
||||
if (!Array.isArray(preferences.type)) {
|
||||
export const matchingMediaTypes = (preferences: RepresentationPreferences, available: string[]):
|
||||
string[] => {
|
||||
const preferredTypes = preferences.type;
|
||||
if (!preferredTypes || Object.keys(preferredTypes).length === 0) {
|
||||
throw new BadRequestHttpError('Output type required for conversion.');
|
||||
}
|
||||
|
||||
const prefMap = preferences.type.reduce((map: Record<string, number>, pref): Record<string, number> => {
|
||||
if (map[pref.value]) {
|
||||
throw new BadRequestHttpError(`Duplicate type preference found: ${pref.value}`);
|
||||
}
|
||||
map[pref.value] = pref.weight;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
// Prevent accidental use of internal types
|
||||
if (!prefMap[INTERNAL_ALL]) {
|
||||
prefMap[INTERNAL_ALL] = 0;
|
||||
if (!preferredTypes[INTERNAL_ALL]) {
|
||||
preferredTypes[INTERNAL_ALL] = 0;
|
||||
}
|
||||
|
||||
// RFC 7231
|
||||
// Media ranges can be overridden by more specific media ranges or
|
||||
// specific media types. If more than one media range applies to a
|
||||
// given type, the most specific reference has precedence.
|
||||
const weightedSupported = types.map((type): RepresentationPreference => {
|
||||
const weightedSupported = available.map((type): [string, number] => {
|
||||
const match = /^([^/]+)\/([^\s;]+)/u.exec(type);
|
||||
if (!match) {
|
||||
throw new InternalServerError(`Unexpected type preference: ${type}`);
|
||||
}
|
||||
const [ , main, sub ] = match;
|
||||
const weight = prefMap[type] ?? prefMap[`${main}/${sub}`] ?? prefMap[`${main}/*`] ?? prefMap['*/*'] ?? 0;
|
||||
return { value: type, weight };
|
||||
const weight =
|
||||
preferredTypes[type] ??
|
||||
preferredTypes[`${main}/${sub}`] ??
|
||||
preferredTypes[`${main}/*`] ??
|
||||
preferredTypes['*/*'] ??
|
||||
0;
|
||||
return [ type, weight ];
|
||||
});
|
||||
|
||||
// Return all non-zero preferences in descending order of weight
|
||||
return weightedSupported
|
||||
.filter((pref): boolean => pref.weight !== 0)
|
||||
.sort((prefA, prefB): number => prefB.weight - prefA.weight);
|
||||
.filter(([ , weight ]): boolean => weight !== 0)
|
||||
.sort(([ , weightA ], [ , weightB ]): number => weightB - weightA)
|
||||
.map(([ type ]): string => type);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Readable } from 'stream';
|
||||
import rdfSerializer from 'rdf-serialize';
|
||||
import type { Representation } from '../../ldp/representation/Representation';
|
||||
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
||||
import type { RepresentationPreference } from '../../ldp/representation/RepresentationPreference';
|
||||
import type { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences';
|
||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||
import { guardStream } from '../../util/GuardedStream';
|
||||
@@ -14,11 +15,11 @@ import { TypedRepresentationConverter } from './TypedRepresentationConverter';
|
||||
* Converts `internal/quads` to most major RDF serializations.
|
||||
*/
|
||||
export class QuadToRdfConverter extends TypedRepresentationConverter {
|
||||
public async getInputTypes(): Promise<Record<string, number>> {
|
||||
public async getInputTypes(): Promise<RepresentationPreference> {
|
||||
return { [INTERNAL_QUADS]: 1 };
|
||||
}
|
||||
|
||||
public async getOutputTypes(): Promise<Record<string, number>> {
|
||||
public async getOutputTypes(): Promise<RepresentationPreference> {
|
||||
return rdfSerializer.getContentTypesPrioritized();
|
||||
}
|
||||
|
||||
@@ -27,7 +28,7 @@ export class QuadToRdfConverter extends TypedRepresentationConverter {
|
||||
}
|
||||
|
||||
private async quadsToRdf(quads: Representation, preferences: RepresentationPreferences): Promise<Representation> {
|
||||
const contentType = matchingMediaTypes(preferences, await rdfSerializer.getContentTypes())[0].value;
|
||||
const contentType = matchingMediaTypes(preferences, await rdfSerializer.getContentTypes())[0];
|
||||
const metadata = new RepresentationMetadata(quads.metadata, { [CONTENT_TYPE]: contentType });
|
||||
return {
|
||||
binary: true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RepresentationPreference } from '../../ldp/representation/RepresentationPreference';
|
||||
import { supportsConversion } from './ConversionUtil';
|
||||
import { RepresentationConverter } from './RepresentationConverter';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
@@ -7,18 +8,14 @@ import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
*/
|
||||
export abstract class TypedRepresentationConverter extends RepresentationConverter {
|
||||
/**
|
||||
* Get a hash of all supported input content types for this converter, mapped to a numerical priority.
|
||||
* The priority weight goes from 0 up to 1.
|
||||
* @returns A promise resolving to a hash mapping content type to a priority number.
|
||||
* Gets the supported input content types for this converter, mapped to a numerical priority.
|
||||
*/
|
||||
public abstract getInputTypes(): Promise<Record<string, number>>;
|
||||
public abstract getInputTypes(): Promise<RepresentationPreference>;
|
||||
|
||||
/**
|
||||
* Get a hash of all supported output content types for this converter, mapped to a numerical priority.
|
||||
* The priority weight goes from 0 up to 1.
|
||||
* @returns A promise resolving to a hash mapping content type to a priority number.
|
||||
* Gets the supported output content types for this converter, mapped to a numerical quality.
|
||||
*/
|
||||
public abstract getOutputTypes(): Promise<Record<string, number>>;
|
||||
public abstract getOutputTypes(): Promise<RepresentationPreference>;
|
||||
|
||||
/**
|
||||
* Verifies whether this converter supports the input.
|
||||
|
||||
@@ -86,7 +86,8 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
|
||||
const store = new Store<BaseQuad>();
|
||||
try {
|
||||
// Read the quads of the current representation
|
||||
const quads = await this.source.getRepresentation(identifier, { type: [{ value: INTERNAL_QUADS, weight: 1 }]});
|
||||
const quads = await this.source.getRepresentation(identifier,
|
||||
{ type: { [INTERNAL_QUADS]: 1 }});
|
||||
const importEmitter = store.import(quads.data);
|
||||
await new Promise((resolve, reject): void => {
|
||||
importEmitter.on('end', resolve);
|
||||
|
||||
@@ -15,7 +15,7 @@ export class PreferenceSupport {
|
||||
private readonly converter: RepresentationConverter;
|
||||
|
||||
public constructor(type: string, converter: RepresentationConverter) {
|
||||
this.preferences = { type: [{ value: type, weight: 1 }]};
|
||||
this.preferences = { type: { [type]: 1 }};
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('A ChainedConverter', (): void => {
|
||||
|
||||
const result = await converter.handleSafe({
|
||||
representation,
|
||||
preferences: { type: [{ value: 'text/turtle', weight: 1 }]},
|
||||
preferences: { type: { 'text/turtle': 1 }},
|
||||
identifier: { path: 'path' },
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('A ChainedConverter', (): void => {
|
||||
|
||||
const result = await converter.handleSafe({
|
||||
representation,
|
||||
preferences: { type: [{ value: 'application/ld+json', weight: 1 }]},
|
||||
preferences: { type: { 'application/ld+json': 1 }},
|
||||
identifier: { path: 'path' },
|
||||
});
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ describe('A BasicRequestParser with simple input parsers', (): void => {
|
||||
method: 'POST',
|
||||
target: { path: 'http://test.com/' },
|
||||
preferences: {
|
||||
type: [{ value: 'text/turtle', weight: 0.8 }],
|
||||
language: [{ value: 'en-gb', weight: 1 }, { value: 'en', weight: 0.5 }],
|
||||
type: { 'text/turtle': 0.8 },
|
||||
language: { 'en-gb': 1, en: 0.5 },
|
||||
},
|
||||
body: {
|
||||
data: expect.any(Readable),
|
||||
|
||||
@@ -14,33 +14,30 @@ describe('An AcceptPreferenceParser', (): void => {
|
||||
|
||||
it('parses accept headers.', async(): Promise<void> => {
|
||||
await expect(preferenceParser.handle({ headers: { accept: 'audio/*; q=0.2, audio/basic' }} as HttpRequest))
|
||||
.resolves.toEqual({ type: [{ value: 'audio/basic', weight: 1 }, { value: 'audio/*', weight: 0.2 }]});
|
||||
.resolves.toEqual({ type: { 'audio/basic': 1, 'audio/*': 0.2 }});
|
||||
});
|
||||
|
||||
it('parses accept-charset headers.', async(): Promise<void> => {
|
||||
await expect(preferenceParser.handle(
|
||||
{ headers: { 'accept-charset': 'iso-8859-5, unicode-1-1;q=0.8' }} as unknown as HttpRequest,
|
||||
)).resolves.toEqual({ charset: [{ value: 'iso-8859-5', weight: 1 }, { value: 'unicode-1-1', weight: 0.8 }]});
|
||||
)).resolves.toEqual({ charset: { 'iso-8859-5': 1, 'unicode-1-1': 0.8 }});
|
||||
});
|
||||
|
||||
it('parses accept-datetime headers.', async(): Promise<void> => {
|
||||
await expect(preferenceParser.handle(
|
||||
{ headers: { 'accept-datetime': 'Tue, 20 Mar 2001 20:35:00 GMT' }} as unknown as HttpRequest,
|
||||
)).resolves.toEqual({ datetime: [{ value: 'Tue, 20 Mar 2001 20:35:00 GMT', weight: 1 }]});
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
)).resolves.toEqual({ datetime: { 'Tue, 20 Mar 2001 20:35:00 GMT': 1 }});
|
||||
});
|
||||
|
||||
it('parses accept-encoding headers.', async(): Promise<void> => {
|
||||
await expect(preferenceParser.handle(
|
||||
{ headers: { 'accept-encoding': 'gzip;q=1.0, identity; q=0.5, *;q=0' }} as unknown as HttpRequest,
|
||||
)).resolves.toEqual(
|
||||
{ encoding: [{ value: 'gzip', weight: 1 }, { value: 'identity', weight: 0.5 }, { value: '*', weight: 0 }]},
|
||||
);
|
||||
)).resolves.toEqual({ encoding: { gzip: 1, identity: 0.5, '*': 0 }});
|
||||
});
|
||||
|
||||
it('parses accept-language headers.', async(): Promise<void> => {
|
||||
await expect(preferenceParser.handle({ headers: { 'accept-language': 'da, en-gb;q=0.8, en;q=0.7' }} as HttpRequest))
|
||||
.resolves.toEqual(
|
||||
{ language: [{ value: 'da', weight: 1 }, { value: 'en-gb', weight: 0.8 }, { value: 'en', weight: 0.7 }]},
|
||||
);
|
||||
.resolves.toEqual({ language: { da: 1, 'en-gb': 0.8, en: 0.7 }});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,9 +32,8 @@ describe('A RepresentationConvertingStore', (): void => {
|
||||
});
|
||||
|
||||
it('returns the Representation from the source if no changes are required.', async(): Promise<void> => {
|
||||
const result = await store.getRepresentation({ path: 'path' }, { type: [
|
||||
{ value: 'application/*', weight: 0 }, { value: 'text/turtle', weight: 1 },
|
||||
]});
|
||||
const result = await store.getRepresentation({ path: 'path' },
|
||||
{ type: { 'application/*': 0, 'text/turtle': 1 }});
|
||||
expect(result).toEqual({
|
||||
data: 'data',
|
||||
metadata: expect.any(RepresentationMetadata),
|
||||
@@ -43,7 +42,7 @@ describe('A RepresentationConvertingStore', (): void => {
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(source.getRepresentation).toHaveBeenLastCalledWith(
|
||||
{ path: 'path' },
|
||||
{ type: [{ value: 'application/*', weight: 0 }, { value: 'text/turtle', weight: 1 }]},
|
||||
{ type: { 'application/*': 0, 'text/turtle': 1, 'internal/*': 0 }},
|
||||
undefined,
|
||||
);
|
||||
expect(outConverter.handleSafe).toHaveBeenCalledTimes(0);
|
||||
@@ -64,15 +63,14 @@ describe('A RepresentationConvertingStore', (): void => {
|
||||
});
|
||||
|
||||
it('calls the converter if another output is preferred.', async(): Promise<void> => {
|
||||
await expect(store.getRepresentation({ path: 'path' }, { type: [
|
||||
{ value: 'text/plain', weight: 1 }, { value: 'text/turtle', weight: 0 },
|
||||
]})).resolves.toEqual(convertedOut);
|
||||
await expect(store.getRepresentation({ path: 'path' },
|
||||
{ type: { 'text/plain': 1, 'text/turtle': 0 }})).resolves.toEqual(convertedOut);
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(outConverter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(outConverter.handleSafe).toHaveBeenLastCalledWith({
|
||||
identifier: { path: 'path' },
|
||||
representation: { data: 'data', metadata },
|
||||
preferences: { type: [{ value: 'text/plain', weight: 1 }, { value: 'text/turtle', weight: 0 }]},
|
||||
preferences: { type: { 'text/plain': 1, 'text/turtle': 0, 'internal/*': 0 }},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class DummyConverter extends TypedRepresentationConverter {
|
||||
|
||||
public async handle(input: RepresentationConverterArgs): Promise<Representation> {
|
||||
const metadata = new RepresentationMetadata(input.representation.metadata,
|
||||
{ [CONTENT_TYPE]: input.preferences.type![0].value });
|
||||
{ [CONTENT_TYPE]: Object.keys(input.preferences.type!)[0] });
|
||||
return { ...input.representation, metadata };
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ describe('A ChainedConverter', (): void => {
|
||||
|
||||
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
||||
representation = { metadata } as Representation;
|
||||
preferences = { type: [{ value: 'internal/quads', weight: 1 }]};
|
||||
preferences = { type: { 'internal/quads': 1 }};
|
||||
args = { representation, preferences, identifier: { path: 'path' }};
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
matchingMediaTypes,
|
||||
supportsConversion,
|
||||
} from '../../../../src/storage/conversion/ConversionUtil';
|
||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
|
||||
|
||||
describe('ConversionUtil', (): void => {
|
||||
@@ -29,21 +28,24 @@ describe('ConversionUtil', (): void => {
|
||||
|
||||
it('requires a matching input type.', async(): Promise<void> => {
|
||||
metadata.contentType = 'a/x';
|
||||
const preferences: RepresentationPreferences = { type: [{ value: 'b/x', weight: 1 }]};
|
||||
const preferences: RepresentationPreferences =
|
||||
{ type: { 'b/x': 1 }};
|
||||
expect((): any => supportsConversion({ identifier, representation, preferences }, [ 'c/x' ], [ 'a/x' ]))
|
||||
.toThrow('Can only convert from c/x to a/x.');
|
||||
});
|
||||
|
||||
it('requires a matching output type.', async(): Promise<void> => {
|
||||
metadata.contentType = 'a/x';
|
||||
const preferences: RepresentationPreferences = { type: [{ value: 'b/x', weight: 1 }]};
|
||||
const preferences: RepresentationPreferences =
|
||||
{ type: { 'b/x': 1 }};
|
||||
expect((): any => supportsConversion({ identifier, representation, preferences }, [ 'a/x' ], [ 'c/x' ]))
|
||||
.toThrow('Can only convert from a/x to c/x.');
|
||||
});
|
||||
|
||||
it('succeeds with a valid input and output type.', async(): Promise<void> => {
|
||||
metadata.contentType = 'a/x';
|
||||
const preferences: RepresentationPreferences = { type: [{ value: 'b/x', weight: 1 }]};
|
||||
const preferences: RepresentationPreferences =
|
||||
{ type: { 'b/x': 1 }};
|
||||
expect(supportsConversion({ identifier, representation, preferences }, [ 'a/x' ], [ 'b/x' ]))
|
||||
.toBeUndefined();
|
||||
});
|
||||
@@ -51,55 +53,52 @@ describe('ConversionUtil', (): void => {
|
||||
|
||||
describe('#matchingMediaTypes', (): void => {
|
||||
it('requires type preferences.', async(): Promise<void> => {
|
||||
const preferences: RepresentationPreferences = {};
|
||||
const preferences: RepresentationPreferences =
|
||||
{};
|
||||
expect((): any => matchingMediaTypes(preferences, [ 'a/b' ]))
|
||||
.toThrow('Output type required for conversion.');
|
||||
});
|
||||
|
||||
it('returns matching types if weight > 0.', async(): Promise<void> => {
|
||||
const preferences: RepresentationPreferences = { type:
|
||||
[{ value: 'a/x', weight: 1 }, { value: 'b/x', weight: 0.5 }, { value: 'c/x', weight: 0 }]};
|
||||
expect(matchingMediaTypes(preferences, [ 'b/x', 'c/x' ])).toEqual([{ value: 'b/x', weight: 0.5 }]);
|
||||
const preferences: RepresentationPreferences =
|
||||
{ type: { 'a/x': 1, 'b/x': 0.5, 'c/x': 0 }};
|
||||
expect(matchingMediaTypes(preferences, [ 'b/x', 'c/x' ]))
|
||||
.toEqual([ 'b/x' ]);
|
||||
});
|
||||
|
||||
it('sorts by descending weight.', async(): Promise<void> => {
|
||||
const preferences: RepresentationPreferences = { type:
|
||||
[{ value: 'a/x', weight: 1 }, { value: 'b/x', weight: 0.5 }, { value: 'c/x', weight: 0.8 }]};
|
||||
expect(matchingMediaTypes(preferences, [ 'a/x', 'b/x', 'c/x' ]))
|
||||
.toEqual([{ value: 'a/x', weight: 1 }, { value: 'c/x', weight: 0.8 }, { value: 'b/x', weight: 0.5 }]);
|
||||
});
|
||||
|
||||
it('errors if there are duplicate preferences.', async(): Promise<void> => {
|
||||
const preferences: RepresentationPreferences =
|
||||
{ type: [{ value: 'b/x', weight: 1 }, { value: 'b/x', weight: 0 }]};
|
||||
expect((): any => matchingMediaTypes(preferences, [ 'b/x' ]))
|
||||
.toThrow(new BadRequestHttpError(`Duplicate type preference found: b/x`));
|
||||
{ type: { 'a/x': 1, 'b/x': 0.5, 'c/x': 0.8 }};
|
||||
expect(matchingMediaTypes(preferences, [ 'a/x', 'b/x', 'c/x' ]))
|
||||
.toEqual([ 'a/x', 'c/x', 'b/x' ]);
|
||||
});
|
||||
|
||||
it('errors if there invalid types.', async(): Promise<void> => {
|
||||
const preferences: RepresentationPreferences =
|
||||
{ type: [{ value: 'b/x', weight: 1 }]};
|
||||
{ type: { 'b/x': 1 }};
|
||||
expect((): any => matchingMediaTypes(preferences, [ 'noType' ]))
|
||||
.toThrow(new InternalServerError(`Unexpected type preference: noType`));
|
||||
});
|
||||
|
||||
it('filters out internal types.', async(): Promise<void> => {
|
||||
const preferences: RepresentationPreferences = { type: [{ value: '*/*', weight: 1 }]};
|
||||
expect(matchingMediaTypes(preferences, [ 'a/x', 'internal/quads' ])).toEqual([{ value: 'a/x', weight: 1 }]);
|
||||
const preferences: RepresentationPreferences =
|
||||
{ type: { '*/*': 1 }};
|
||||
expect(matchingMediaTypes(preferences, [ 'a/x', 'internal/quads' ]))
|
||||
.toEqual([ 'a/x' ]);
|
||||
});
|
||||
|
||||
it('keeps internal types that are specifically requested.', async(): Promise<void> => {
|
||||
const preferences: RepresentationPreferences =
|
||||
{ type: [{ value: '*/*', weight: 1 }, { value: 'internal/*', weight: 0.5 }]};
|
||||
{ type: { '*/*': 1, 'internal/*': 0.5 }};
|
||||
expect(matchingMediaTypes(preferences, [ 'a/x', 'internal/quads' ]))
|
||||
.toEqual([{ value: 'a/x', weight: 1 }, { value: 'internal/quads', weight: 0.5 }]);
|
||||
.toEqual([ 'a/x', 'internal/quads' ]);
|
||||
});
|
||||
|
||||
it('takes the most relevant weight for a type.', async(): Promise<void> => {
|
||||
const preferences: RepresentationPreferences =
|
||||
{ type: [{ value: '*/*', weight: 1 }, { value: 'internal/quads', weight: 0.5 }]};
|
||||
{ type: { '*/*': 1, 'internal/quads': 0.5 }};
|
||||
expect(matchingMediaTypes(preferences, [ 'a/x', 'internal/quads' ]))
|
||||
.toEqual([{ value: 'a/x', weight: 1 }, { value: 'internal/quads', weight: 0.5 }]);
|
||||
.toEqual([ 'a/x', 'internal/quads' ]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -25,13 +25,13 @@ describe('A QuadToRdfConverter', (): void => {
|
||||
|
||||
it('can handle quad to turtle conversions.', async(): Promise<void> => {
|
||||
const representation = { metadata } as Representation;
|
||||
const preferences: RepresentationPreferences = { type: [{ value: 'text/turtle', weight: 1 }]};
|
||||
const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
|
||||
await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('can handle quad to JSON-LD conversions.', async(): Promise<void> => {
|
||||
const representation = { metadata } as Representation;
|
||||
const preferences: RepresentationPreferences = { type: [{ value: 'application/ld+json', weight: 1 }]};
|
||||
const preferences: RepresentationPreferences = { type: { 'application/ld+json': 1 }};
|
||||
await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('A QuadToRdfConverter', (): void => {
|
||||
) ]),
|
||||
metadata,
|
||||
} as Representation;
|
||||
const preferences: RepresentationPreferences = { type: [{ value: 'text/turtle', weight: 1 }]};
|
||||
const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
|
||||
const result = await converter.handle({ identifier, representation, preferences });
|
||||
expect(result).toMatchObject({
|
||||
binary: true,
|
||||
@@ -67,7 +67,7 @@ describe('A QuadToRdfConverter', (): void => {
|
||||
) ]),
|
||||
metadata,
|
||||
} as Representation;
|
||||
const preferences: RepresentationPreferences = { type: [{ value: 'application/ld+json', weight: 1 }]};
|
||||
const preferences: RepresentationPreferences = { type: { 'application/ld+json': 1 }};
|
||||
const result = await converter.handle({ identifier, representation, preferences });
|
||||
expect(result).toMatchObject({
|
||||
binary: true,
|
||||
|
||||
@@ -28,14 +28,14 @@ describe('A RdfToQuadConverter.test.ts', (): void => {
|
||||
it('can handle turtle to quad conversions.', async(): Promise<void> => {
|
||||
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
||||
const representation = { metadata } as Representation;
|
||||
const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]};
|
||||
const preferences: RepresentationPreferences = { type: { [INTERNAL_QUADS]: 1 }};
|
||||
await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('can handle JSON-LD to quad conversions.', async(): Promise<void> => {
|
||||
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'application/ld+json' });
|
||||
const representation = { metadata } as Representation;
|
||||
const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]};
|
||||
const preferences: RepresentationPreferences = { type: { [INTERNAL_QUADS]: 1 }};
|
||||
await expect(converter.canHandle({ identifier, representation, preferences })).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('A RdfToQuadConverter.test.ts', (): void => {
|
||||
data: streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]),
|
||||
metadata,
|
||||
} as Representation;
|
||||
const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]};
|
||||
const preferences: RepresentationPreferences = { type: { [INTERNAL_QUADS]: 1 }};
|
||||
const result = await converter.handle({ identifier, representation, preferences });
|
||||
expect(result).toEqual({
|
||||
binary: false,
|
||||
@@ -66,7 +66,7 @@ describe('A RdfToQuadConverter.test.ts', (): void => {
|
||||
data: streamifyArray([ '{"@id": "http://test.com/s", "http://test.com/p": { "@id": "http://test.com/o" }}' ]),
|
||||
metadata,
|
||||
} as Representation;
|
||||
const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]};
|
||||
const preferences: RepresentationPreferences = { type: { [INTERNAL_QUADS]: 1 }};
|
||||
const result = await converter.handle({ identifier, representation, preferences });
|
||||
expect(result).toEqual({
|
||||
binary: false,
|
||||
@@ -87,7 +87,7 @@ describe('A RdfToQuadConverter.test.ts', (): void => {
|
||||
data: streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.co' ]),
|
||||
metadata,
|
||||
} as Representation;
|
||||
const preferences: RepresentationPreferences = { type: [{ value: INTERNAL_QUADS, weight: 1 }]};
|
||||
const preferences: RepresentationPreferences = { type: { [INTERNAL_QUADS]: 1 }};
|
||||
const result = await converter.handle({ identifier, representation, preferences });
|
||||
expect(result).toEqual({
|
||||
binary: false,
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('A SparqlUpdatePatchHandler', (): void => {
|
||||
const basicChecks = async(quads: Quad[]): Promise<boolean> => {
|
||||
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(source.getRepresentation).toHaveBeenLastCalledWith(
|
||||
{ path: 'path' }, { type: [{ value: INTERNAL_QUADS, weight: 1 }]},
|
||||
{ path: 'path' }, { type: { [INTERNAL_QUADS]: 1 }},
|
||||
);
|
||||
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(order).toEqual([ 'acquire', 'getRepresentation', 'setRepresentation', 'release' ]);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpE
|
||||
|
||||
describe('A PreferenceSupport', (): void => {
|
||||
const type = 'internal/quads';
|
||||
const preferences: RepresentationPreferences = { type: [{ value: type, weight: 1 }]};
|
||||
const preferences: RepresentationPreferences = { type: { [type]: 1 }};
|
||||
let converter: RepresentationConverter;
|
||||
let support: PreferenceSupport;
|
||||
const identifier: ResourceIdentifier = 'identifier' as any;
|
||||
|
||||
Reference in New Issue
Block a user