refactor: Create BaseTypedRepresentationConverter

This commit is contained in:
Joachim Van Herwegen 2021-10-26 15:27:19 +02:00
parent 3861e64398
commit 27306d6e3f
14 changed files with 102 additions and 84 deletions

View File

@ -250,6 +250,7 @@ export * from './storage/accessors/InMemoryDataAccessor';
export * from './storage/accessors/SparqlDataAccessor'; export * from './storage/accessors/SparqlDataAccessor';
// Storage/Conversion // Storage/Conversion
export * from './storage/conversion/BaseTypedRepresentationConverter';
export * from './storage/conversion/ChainedConverter'; export * from './storage/conversion/ChainedConverter';
export * from './storage/conversion/ConstantConverter'; export * from './storage/conversion/ConstantConverter';
export * from './storage/conversion/ContainerToTemplateConverter'; export * from './storage/conversion/ContainerToTemplateConverter';

View File

@ -0,0 +1,76 @@
import type { ValuePreferences } from '../../http/representation/RepresentationPreferences';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { getConversionTarget, getTypeWeight } from './ConversionUtil';
import { RepresentationConverter } from './RepresentationConverter';
import type { RepresentationConverterArgs } from './RepresentationConverter';
type PromiseOrValue<T> = T | Promise<T>;
type ValuePreferencesArg =
PromiseOrValue<string> |
PromiseOrValue<string[]> |
PromiseOrValue<ValuePreferences>;
async function toValuePreferences(arg: ValuePreferencesArg): Promise<ValuePreferences> {
const resolved = await arg;
if (typeof resolved === 'string') {
return { [resolved]: 1 };
}
if (Array.isArray(resolved)) {
return Object.fromEntries(resolved.map((type): [string, number] => [ type, 1 ]));
}
return resolved;
}
/**
* A {@link RepresentationConverter} that allows requesting the supported types.
*/
export abstract class BaseTypedRepresentationConverter extends RepresentationConverter {
protected inputTypes: Promise<ValuePreferences>;
protected outputTypes: Promise<ValuePreferences>;
public constructor(inputTypes: ValuePreferencesArg = {}, outputTypes: ValuePreferencesArg = {}) {
super();
this.inputTypes = toValuePreferences(inputTypes);
this.outputTypes = toValuePreferences(outputTypes);
}
/**
* Gets the supported input content types for this converter, mapped to a numerical priority.
*/
public async getInputTypes(): Promise<ValuePreferences> {
return this.inputTypes;
}
/**
* Gets the supported output content types for this converter, mapped to a numerical quality.
*/
public async getOutputTypes(): Promise<ValuePreferences> {
return this.outputTypes;
}
/**
* Determines whether the given conversion request is supported,
* given the available content type conversions:
* - Checks if there is a content type for the input.
* - Checks if the input type is supported by the parser.
* - Checks if the parser can produce one of the preferred output types.
* Throws an error with details if conversion is not possible.
*/
public async canHandle(args: RepresentationConverterArgs): Promise<void> {
const types = [ this.getInputTypes(), this.getOutputTypes() ];
const { contentType } = args.representation.metadata;
if (!contentType) {
throw new NotImplementedHttpError('Can not convert data without a Content-Type.');
}
const [ inputTypes, outputTypes ] = await Promise.all(types);
const outputPreferences = args.preferences.type ?? {};
if (getTypeWeight(contentType, inputTypes) === 0 || !getConversionTarget(outputTypes, outputPreferences)) {
throw new NotImplementedHttpError(
`Cannot convert from ${contentType} to ${Object.keys(outputPreferences)
}, only from ${Object.keys(inputTypes)} to ${Object.keys(outputTypes)}.`,
);
}
}
}

View File

@ -11,8 +11,8 @@ import { isContainerIdentifier, isContainerPath } from '../../util/PathUtil';
import { endOfStream } from '../../util/StreamUtil'; import { endOfStream } from '../../util/StreamUtil';
import type { TemplateEngine } from '../../util/templates/TemplateEngine'; import type { TemplateEngine } from '../../util/templates/TemplateEngine';
import { LDP } from '../../util/Vocabularies'; import { LDP } from '../../util/Vocabularies';
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
import type { RepresentationConverterArgs } from './RepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
interface ResourceDetails { interface ResourceDetails {
name: string; name: string;
@ -23,7 +23,7 @@ interface ResourceDetails {
/** /**
* A {@link RepresentationConverter} that creates a templated representation of a container. * A {@link RepresentationConverter} that creates a templated representation of a container.
*/ */
export class ContainerToTemplateConverter extends TypedRepresentationConverter { export class ContainerToTemplateConverter extends BaseTypedRepresentationConverter {
private readonly identifierStrategy: IdentifierStrategy; private readonly identifierStrategy: IdentifierStrategy;
private readonly templateEngine: TemplateEngine; private readonly templateEngine: TemplateEngine;
private readonly contentType: string; private readonly contentType: string;

View File

@ -3,13 +3,13 @@ import type { Representation } from '../../http/representation/Representation';
import { APPLICATION_JSON, INTERNAL_ERROR } from '../../util/ContentTypes'; import { APPLICATION_JSON, INTERNAL_ERROR } from '../../util/ContentTypes';
import { HttpError } from '../../util/errors/HttpError'; import { HttpError } from '../../util/errors/HttpError';
import { getSingleItem } from '../../util/StreamUtil'; import { getSingleItem } from '../../util/StreamUtil';
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
import type { RepresentationConverterArgs } from './RepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
/** /**
* Converts an Error object to JSON by copying its fields. * Converts an Error object to JSON by copying its fields.
*/ */
export class ErrorToJsonConverter extends TypedRepresentationConverter { export class ErrorToJsonConverter extends BaseTypedRepresentationConverter {
public constructor() { public constructor() {
super(INTERNAL_ERROR, APPLICATION_JSON); super(INTERNAL_ERROR, APPLICATION_JSON);
} }

View File

@ -4,13 +4,13 @@ import { RepresentationMetadata } from '../../http/representation/Representation
import { INTERNAL_ERROR, INTERNAL_QUADS } from '../../util/ContentTypes'; import { INTERNAL_ERROR, INTERNAL_QUADS } from '../../util/ContentTypes';
import { getSingleItem } from '../../util/StreamUtil'; import { getSingleItem } from '../../util/StreamUtil';
import { DC, SOLID_ERROR } from '../../util/Vocabularies'; import { DC, SOLID_ERROR } from '../../util/Vocabularies';
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
import type { RepresentationConverterArgs } from './RepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
/** /**
* Converts an error object into quads by creating a triple for each of name/message/stack. * Converts an error object into quads by creating a triple for each of name/message/stack.
*/ */
export class ErrorToQuadConverter extends TypedRepresentationConverter { export class ErrorToQuadConverter extends BaseTypedRepresentationConverter {
public constructor() { public constructor() {
super(INTERNAL_ERROR, INTERNAL_QUADS); super(INTERNAL_ERROR, INTERNAL_QUADS);
} }

View File

@ -6,8 +6,8 @@ import { HttpError } from '../../util/errors/HttpError';
import { modulePathPlaceholder } from '../../util/PathUtil'; import { modulePathPlaceholder } from '../../util/PathUtil';
import { getSingleItem } from '../../util/StreamUtil'; import { getSingleItem } from '../../util/StreamUtil';
import type { TemplateEngine } from '../../util/templates/TemplateEngine'; import type { TemplateEngine } from '../../util/templates/TemplateEngine';
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
import type { RepresentationConverterArgs } from './RepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
// Fields optional due to https://github.com/LinkedSoftwareDependencies/Components.js/issues/20 // Fields optional due to https://github.com/LinkedSoftwareDependencies/Components.js/issues/20
export interface TemplateOptions { export interface TemplateOptions {
@ -35,7 +35,7 @@ const DEFAULT_TEMPLATE_OPTIONS: TemplateOptions = {
* That result will be passed as an additional parameter to the main templating call, * That result will be passed as an additional parameter to the main templating call,
* using the variable `codeMessage`. * using the variable `codeMessage`.
*/ */
export class ErrorToTemplateConverter extends TypedRepresentationConverter { export class ErrorToTemplateConverter extends BaseTypedRepresentationConverter {
private readonly templateEngine: TemplateEngine; private readonly templateEngine: TemplateEngine;
private readonly mainTemplatePath: string; private readonly mainTemplatePath: string;
private readonly codeTemplatesPath: string; private readonly codeTemplatesPath: string;

View File

@ -5,14 +5,14 @@ import { RepresentationMetadata } from '../../http/representation/Representation
import { APPLICATION_JSON, APPLICATION_X_WWW_FORM_URLENCODED } from '../../util/ContentTypes'; import { APPLICATION_JSON, APPLICATION_X_WWW_FORM_URLENCODED } from '../../util/ContentTypes';
import { readableToString } from '../../util/StreamUtil'; import { readableToString } from '../../util/StreamUtil';
import { CONTENT_TYPE } from '../../util/Vocabularies'; import { CONTENT_TYPE } from '../../util/Vocabularies';
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
import type { RepresentationConverterArgs } from './RepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
/** /**
* Converts application/x-www-form-urlencoded data to application/json. * Converts application/x-www-form-urlencoded data to application/json.
* Due to the nature of form data, the result will be a simple key/value JSON object. * Due to the nature of form data, the result will be a simple key/value JSON object.
*/ */
export class FormToJsonConverter extends TypedRepresentationConverter { export class FormToJsonConverter extends BaseTypedRepresentationConverter {
public constructor() { public constructor() {
super(APPLICATION_X_WWW_FORM_URLENCODED, APPLICATION_JSON); super(APPLICATION_X_WWW_FORM_URLENCODED, APPLICATION_JSON);
} }

View File

@ -4,8 +4,8 @@ import type { Representation } from '../../http/representation/Representation';
import { TEXT_HTML, TEXT_MARKDOWN } from '../../util/ContentTypes'; import { TEXT_HTML, TEXT_MARKDOWN } from '../../util/ContentTypes';
import { readableToString } from '../../util/StreamUtil'; import { readableToString } from '../../util/StreamUtil';
import type { TemplateEngine } from '../../util/templates/TemplateEngine'; import type { TemplateEngine } from '../../util/templates/TemplateEngine';
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
import type { RepresentationConverterArgs } from './RepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
/** /**
* Converts Markdown data to HTML. * Converts Markdown data to HTML.
@ -13,7 +13,7 @@ import { TypedRepresentationConverter } from './TypedRepresentationConverter';
* A standard Markdown string will be converted to a <p> tag, so html and body tags should be part of the template. * A standard Markdown string will be converted to a <p> tag, so html and body tags should be part of the template.
* In case the Markdown body starts with a header (#), that value will also be used as `title` parameter. * In case the Markdown body starts with a header (#), that value will also be used as `title` parameter.
*/ */
export class MarkdownToHtmlConverter extends TypedRepresentationConverter { export class MarkdownToHtmlConverter extends BaseTypedRepresentationConverter {
private readonly templateEngine: TemplateEngine; private readonly templateEngine: TemplateEngine;
public constructor(templateEngine: TemplateEngine) { public constructor(templateEngine: TemplateEngine) {

View File

@ -7,14 +7,14 @@ import type { ValuePreferences } from '../../http/representation/RepresentationP
import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { pipeSafely } from '../../util/StreamUtil'; import { pipeSafely } from '../../util/StreamUtil';
import { PREFERRED_PREFIX_TERM } from '../../util/Vocabularies'; import { PREFERRED_PREFIX_TERM } from '../../util/Vocabularies';
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
import { getConversionTarget } from './ConversionUtil'; import { getConversionTarget } from './ConversionUtil';
import type { RepresentationConverterArgs } from './RepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
/** /**
* Converts `internal/quads` to most major RDF serializations. * Converts `internal/quads` to most major RDF serializations.
*/ */
export class QuadToRdfConverter extends TypedRepresentationConverter { export class QuadToRdfConverter extends BaseTypedRepresentationConverter {
private readonly outputPreferences?: ValuePreferences; private readonly outputPreferences?: ValuePreferences;
public constructor(options: { outputPreferences?: Record<string, number> } = {}) { public constructor(options: { outputPreferences?: Record<string, number> } = {}) {

View File

@ -5,13 +5,13 @@ import type { Representation } from '../../http/representation/Representation';
import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
import { pipeSafely } from '../../util/StreamUtil'; import { pipeSafely } from '../../util/StreamUtil';
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
import type { RepresentationConverterArgs } from './RepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
/** /**
* Converts most major RDF serializations to `internal/quads`. * Converts most major RDF serializations to `internal/quads`.
*/ */
export class RdfToQuadConverter extends TypedRepresentationConverter { export class RdfToQuadConverter extends BaseTypedRepresentationConverter {
public constructor() { public constructor() {
super(rdfParser.getContentTypesPrioritized(), INTERNAL_QUADS); super(rdfParser.getContentTypesPrioritized(), INTERNAL_QUADS);
} }

View File

@ -1,76 +1,17 @@
import type { ValuePreferences } from '../../http/representation/RepresentationPreferences'; import type { ValuePreferences } from '../../http/representation/RepresentationPreferences';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { getConversionTarget, getTypeWeight } from './ConversionUtil';
import { RepresentationConverter } from './RepresentationConverter'; import { RepresentationConverter } from './RepresentationConverter';
import type { RepresentationConverterArgs } from './RepresentationConverter';
type PromiseOrValue<T> = T | Promise<T>;
type ValuePreferencesArg =
PromiseOrValue<string> |
PromiseOrValue<string[]> |
PromiseOrValue<ValuePreferences>;
async function toValuePreferences(arg: ValuePreferencesArg): Promise<ValuePreferences> {
const resolved = await arg;
if (typeof resolved === 'string') {
return { [resolved]: 1 };
}
if (Array.isArray(resolved)) {
return Object.fromEntries(resolved.map((type): [string, number] => [ type, 1 ]));
}
return resolved;
}
/** /**
* A {@link RepresentationConverter} that allows requesting the supported types. * A {@link RepresentationConverter} that allows requesting the supported types.
*/ */
export abstract class TypedRepresentationConverter extends RepresentationConverter { export abstract class TypedRepresentationConverter extends RepresentationConverter {
protected inputTypes: Promise<ValuePreferences>;
protected outputTypes: Promise<ValuePreferences>;
public constructor(inputTypes: ValuePreferencesArg = {}, outputTypes: ValuePreferencesArg = {}) {
super();
this.inputTypes = toValuePreferences(inputTypes);
this.outputTypes = toValuePreferences(outputTypes);
}
/** /**
* Gets the supported input content types for this converter, mapped to a numerical priority. * Gets the supported input content types for this converter, mapped to a numerical priority.
*/ */
public async getInputTypes(): Promise<ValuePreferences> { public abstract getInputTypes(): Promise<ValuePreferences>;
return this.inputTypes;
}
/** /**
* Gets the supported output content types for this converter, mapped to a numerical quality. * Gets the supported output content types for this converter, mapped to a numerical quality.
*/ */
public async getOutputTypes(): Promise<ValuePreferences> { public abstract getOutputTypes(): Promise<ValuePreferences>;
return this.outputTypes;
}
/**
* Determines whether the given conversion request is supported,
* given the available content type conversions:
* - Checks if there is a content type for the input.
* - Checks if the input type is supported by the parser.
* - Checks if the parser can produce one of the preferred output types.
* Throws an error with details if conversion is not possible.
*/
public async canHandle(args: RepresentationConverterArgs): Promise<void> {
const types = [ this.getInputTypes(), this.getOutputTypes() ];
const { contentType } = args.representation.metadata;
if (!contentType) {
throw new NotImplementedHttpError('Can not convert data without a Content-Type.');
}
const [ inputTypes, outputTypes ] = await Promise.all(types);
const outputPreferences = args.preferences.type ?? {};
if (getTypeWeight(contentType, inputTypes) === 0 || !getConversionTarget(outputTypes, outputPreferences)) {
throw new NotImplementedHttpError(
`Cannot convert from ${contentType} to ${Object.keys(outputPreferences)
}, only from ${Object.keys(inputTypes)} to ${Object.keys(outputTypes)}.`,
);
}
}
} }

View File

@ -1,6 +1,5 @@
import { import {
RepresentationMetadata, RepresentationMetadata,
TypedRepresentationConverter,
readableToString, readableToString,
ChainedConverter, ChainedConverter,
guardedStreamFrom, guardedStreamFrom,
@ -12,6 +11,7 @@ import {
import type { Representation, import type { Representation,
RepresentationConverterArgs, RepresentationConverterArgs,
Logger } from '../../src'; Logger } from '../../src';
import { BaseTypedRepresentationConverter } from '../../src/storage/conversion/BaseTypedRepresentationConverter';
jest.mock('../../src/logging/LogUtil', (): any => { jest.mock('../../src/logging/LogUtil', (): any => {
const logger: Logger = const logger: Logger =
@ -20,7 +20,7 @@ jest.mock('../../src/logging/LogUtil', (): any => {
}); });
const logger: jest.Mocked<Logger> = getLoggerFor('GuardedStream') as any; const logger: jest.Mocked<Logger> = getLoggerFor('GuardedStream') as any;
class DummyConverter extends TypedRepresentationConverter { class DummyConverter extends BaseTypedRepresentationConverter {
public constructor() { public constructor() {
super('*/*', 'custom/type'); super('*/*', 'custom/type');
} }

View File

@ -1,12 +1,12 @@
import { BaseTypedRepresentationConverter } from '../../../../src/storage/conversion/BaseTypedRepresentationConverter';
import type { RepresentationConverterArgs } from '../../../../src/storage/conversion/RepresentationConverter'; import type { RepresentationConverterArgs } from '../../../../src/storage/conversion/RepresentationConverter';
import { TypedRepresentationConverter } from '../../../../src/storage/conversion/TypedRepresentationConverter';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
class CustomTypedRepresentationConverter extends TypedRepresentationConverter { class CustomTypedRepresentationConverter extends BaseTypedRepresentationConverter {
public handle = jest.fn(); public handle = jest.fn();
} }
describe('A TypedRepresentationConverter', (): void => { describe('A BaseTypedRepresentationConverter', (): void => {
it('defaults to allowing everything.', async(): Promise<void> => { it('defaults to allowing everything.', async(): Promise<void> => {
const converter = new CustomTypedRepresentationConverter(); const converter = new CustomTypedRepresentationConverter();
await expect(converter.getInputTypes()).resolves.toEqual({ await expect(converter.getInputTypes()).resolves.toEqual({

View File

@ -4,13 +4,13 @@ import type {
RepresentationPreferences, RepresentationPreferences,
ValuePreferences, ValuePreferences,
} from '../../../../src/http/representation/RepresentationPreferences'; } from '../../../../src/http/representation/RepresentationPreferences';
import { BaseTypedRepresentationConverter } from '../../../../src/storage/conversion/BaseTypedRepresentationConverter';
import { ChainedConverter } from '../../../../src/storage/conversion/ChainedConverter'; import { ChainedConverter } from '../../../../src/storage/conversion/ChainedConverter';
import { matchesMediaType } from '../../../../src/storage/conversion/ConversionUtil'; import { matchesMediaType } from '../../../../src/storage/conversion/ConversionUtil';
import type { RepresentationConverterArgs } from '../../../../src/storage/conversion/RepresentationConverter'; import type { RepresentationConverterArgs } from '../../../../src/storage/conversion/RepresentationConverter';
import { TypedRepresentationConverter } from '../../../../src/storage/conversion/TypedRepresentationConverter';
import { CONTENT_TYPE } from '../../../../src/util/Vocabularies'; import { CONTENT_TYPE } from '../../../../src/util/Vocabularies';
class DummyConverter extends TypedRepresentationConverter { class DummyConverter extends BaseTypedRepresentationConverter {
private readonly inTypes: ValuePreferences; private readonly inTypes: ValuePreferences;
private readonly outTypes: ValuePreferences; private readonly outTypes: ValuePreferences;