From 27306d6e3f6f3dda09914e078151a8d07e111869 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 26 Oct 2021 15:27:19 +0200 Subject: [PATCH] refactor: Create BaseTypedRepresentationConverter --- src/index.ts | 1 + .../BaseTypedRepresentationConverter.ts | 76 +++++++++++++++++++ .../ContainerToTemplateConverter.ts | 4 +- .../conversion/ErrorToJsonConverter.ts | 4 +- .../conversion/ErrorToQuadConverter.ts | 4 +- .../conversion/ErrorToTemplateConverter.ts | 4 +- src/storage/conversion/FormToJsonConverter.ts | 4 +- .../conversion/MarkdownToHtmlConverter.ts | 4 +- src/storage/conversion/QuadToRdfConverter.ts | 4 +- src/storage/conversion/RdfToQuadConverter.ts | 4 +- .../TypedRepresentationConverter.ts | 63 +-------------- test/integration/GuardedStream.test.ts | 4 +- ... BaseTypedRepresentationConverter.test.ts} | 6 +- .../conversion/ChainedConverter.test.ts | 4 +- 14 files changed, 102 insertions(+), 84 deletions(-) create mode 100644 src/storage/conversion/BaseTypedRepresentationConverter.ts rename test/unit/storage/conversion/{TypedRepresentationConverter.test.ts => BaseTypedRepresentationConverter.test.ts} (92%) diff --git a/src/index.ts b/src/index.ts index 9b2617990..1fe5047ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -250,6 +250,7 @@ export * from './storage/accessors/InMemoryDataAccessor'; export * from './storage/accessors/SparqlDataAccessor'; // Storage/Conversion +export * from './storage/conversion/BaseTypedRepresentationConverter'; export * from './storage/conversion/ChainedConverter'; export * from './storage/conversion/ConstantConverter'; export * from './storage/conversion/ContainerToTemplateConverter'; diff --git a/src/storage/conversion/BaseTypedRepresentationConverter.ts b/src/storage/conversion/BaseTypedRepresentationConverter.ts new file mode 100644 index 000000000..3fc2a769f --- /dev/null +++ b/src/storage/conversion/BaseTypedRepresentationConverter.ts @@ -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 | Promise; +type ValuePreferencesArg = + PromiseOrValue | + PromiseOrValue | + PromiseOrValue; + +async function toValuePreferences(arg: ValuePreferencesArg): Promise { + 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; + protected outputTypes: Promise; + + 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 { + return this.inputTypes; + } + + /** + * Gets the supported output content types for this converter, mapped to a numerical quality. + */ + public async getOutputTypes(): Promise { + 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 { + 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)}.`, + ); + } + } +} diff --git a/src/storage/conversion/ContainerToTemplateConverter.ts b/src/storage/conversion/ContainerToTemplateConverter.ts index d4e0f01e7..8dbafeaf1 100644 --- a/src/storage/conversion/ContainerToTemplateConverter.ts +++ b/src/storage/conversion/ContainerToTemplateConverter.ts @@ -11,8 +11,8 @@ import { isContainerIdentifier, isContainerPath } from '../../util/PathUtil'; import { endOfStream } from '../../util/StreamUtil'; import type { TemplateEngine } from '../../util/templates/TemplateEngine'; import { LDP } from '../../util/Vocabularies'; +import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter'; -import { TypedRepresentationConverter } from './TypedRepresentationConverter'; interface ResourceDetails { name: string; @@ -23,7 +23,7 @@ interface ResourceDetails { /** * 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 templateEngine: TemplateEngine; private readonly contentType: string; diff --git a/src/storage/conversion/ErrorToJsonConverter.ts b/src/storage/conversion/ErrorToJsonConverter.ts index a2f7c9c7f..2f33c44f8 100644 --- a/src/storage/conversion/ErrorToJsonConverter.ts +++ b/src/storage/conversion/ErrorToJsonConverter.ts @@ -3,13 +3,13 @@ import type { Representation } from '../../http/representation/Representation'; import { APPLICATION_JSON, INTERNAL_ERROR } from '../../util/ContentTypes'; import { HttpError } from '../../util/errors/HttpError'; import { getSingleItem } from '../../util/StreamUtil'; +import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter'; -import { TypedRepresentationConverter } from './TypedRepresentationConverter'; /** * Converts an Error object to JSON by copying its fields. */ -export class ErrorToJsonConverter extends TypedRepresentationConverter { +export class ErrorToJsonConverter extends BaseTypedRepresentationConverter { public constructor() { super(INTERNAL_ERROR, APPLICATION_JSON); } diff --git a/src/storage/conversion/ErrorToQuadConverter.ts b/src/storage/conversion/ErrorToQuadConverter.ts index aace74b99..5ebdfb789 100644 --- a/src/storage/conversion/ErrorToQuadConverter.ts +++ b/src/storage/conversion/ErrorToQuadConverter.ts @@ -4,13 +4,13 @@ import { RepresentationMetadata } from '../../http/representation/Representation import { INTERNAL_ERROR, INTERNAL_QUADS } from '../../util/ContentTypes'; import { getSingleItem } from '../../util/StreamUtil'; import { DC, SOLID_ERROR } from '../../util/Vocabularies'; +import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter'; 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. */ -export class ErrorToQuadConverter extends TypedRepresentationConverter { +export class ErrorToQuadConverter extends BaseTypedRepresentationConverter { public constructor() { super(INTERNAL_ERROR, INTERNAL_QUADS); } diff --git a/src/storage/conversion/ErrorToTemplateConverter.ts b/src/storage/conversion/ErrorToTemplateConverter.ts index a72ec4456..923226fa2 100644 --- a/src/storage/conversion/ErrorToTemplateConverter.ts +++ b/src/storage/conversion/ErrorToTemplateConverter.ts @@ -6,8 +6,8 @@ import { HttpError } from '../../util/errors/HttpError'; import { modulePathPlaceholder } from '../../util/PathUtil'; import { getSingleItem } from '../../util/StreamUtil'; import type { TemplateEngine } from '../../util/templates/TemplateEngine'; +import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter'; -import { TypedRepresentationConverter } from './TypedRepresentationConverter'; // Fields optional due to https://github.com/LinkedSoftwareDependencies/Components.js/issues/20 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, * using the variable `codeMessage`. */ -export class ErrorToTemplateConverter extends TypedRepresentationConverter { +export class ErrorToTemplateConverter extends BaseTypedRepresentationConverter { private readonly templateEngine: TemplateEngine; private readonly mainTemplatePath: string; private readonly codeTemplatesPath: string; diff --git a/src/storage/conversion/FormToJsonConverter.ts b/src/storage/conversion/FormToJsonConverter.ts index a964d5cc9..48e8c8f4b 100644 --- a/src/storage/conversion/FormToJsonConverter.ts +++ b/src/storage/conversion/FormToJsonConverter.ts @@ -5,14 +5,14 @@ import { RepresentationMetadata } from '../../http/representation/Representation import { APPLICATION_JSON, APPLICATION_X_WWW_FORM_URLENCODED } from '../../util/ContentTypes'; import { readableToString } from '../../util/StreamUtil'; import { CONTENT_TYPE } from '../../util/Vocabularies'; +import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter'; -import { TypedRepresentationConverter } from './TypedRepresentationConverter'; /** * 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. */ -export class FormToJsonConverter extends TypedRepresentationConverter { +export class FormToJsonConverter extends BaseTypedRepresentationConverter { public constructor() { super(APPLICATION_X_WWW_FORM_URLENCODED, APPLICATION_JSON); } diff --git a/src/storage/conversion/MarkdownToHtmlConverter.ts b/src/storage/conversion/MarkdownToHtmlConverter.ts index 1bbae4d2c..754bc5248 100644 --- a/src/storage/conversion/MarkdownToHtmlConverter.ts +++ b/src/storage/conversion/MarkdownToHtmlConverter.ts @@ -4,8 +4,8 @@ import type { Representation } from '../../http/representation/Representation'; import { TEXT_HTML, TEXT_MARKDOWN } from '../../util/ContentTypes'; import { readableToString } from '../../util/StreamUtil'; import type { TemplateEngine } from '../../util/templates/TemplateEngine'; +import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter'; -import { TypedRepresentationConverter } from './TypedRepresentationConverter'; /** * Converts Markdown data to HTML. @@ -13,7 +13,7 @@ import { TypedRepresentationConverter } from './TypedRepresentationConverter'; * A standard Markdown string will be converted to a

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. */ -export class MarkdownToHtmlConverter extends TypedRepresentationConverter { +export class MarkdownToHtmlConverter extends BaseTypedRepresentationConverter { private readonly templateEngine: TemplateEngine; public constructor(templateEngine: TemplateEngine) { diff --git a/src/storage/conversion/QuadToRdfConverter.ts b/src/storage/conversion/QuadToRdfConverter.ts index 512832b44..e343f3b67 100644 --- a/src/storage/conversion/QuadToRdfConverter.ts +++ b/src/storage/conversion/QuadToRdfConverter.ts @@ -7,14 +7,14 @@ import type { ValuePreferences } from '../../http/representation/RepresentationP import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { pipeSafely } from '../../util/StreamUtil'; import { PREFERRED_PREFIX_TERM } from '../../util/Vocabularies'; +import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter'; import { getConversionTarget } from './ConversionUtil'; import type { RepresentationConverterArgs } from './RepresentationConverter'; -import { TypedRepresentationConverter } from './TypedRepresentationConverter'; /** * Converts `internal/quads` to most major RDF serializations. */ -export class QuadToRdfConverter extends TypedRepresentationConverter { +export class QuadToRdfConverter extends BaseTypedRepresentationConverter { private readonly outputPreferences?: ValuePreferences; public constructor(options: { outputPreferences?: Record } = {}) { diff --git a/src/storage/conversion/RdfToQuadConverter.ts b/src/storage/conversion/RdfToQuadConverter.ts index 521cd4865..19295bd96 100644 --- a/src/storage/conversion/RdfToQuadConverter.ts +++ b/src/storage/conversion/RdfToQuadConverter.ts @@ -5,13 +5,13 @@ import type { Representation } from '../../http/representation/Representation'; import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { pipeSafely } from '../../util/StreamUtil'; +import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter'; import type { RepresentationConverterArgs } from './RepresentationConverter'; -import { TypedRepresentationConverter } from './TypedRepresentationConverter'; /** * Converts most major RDF serializations to `internal/quads`. */ -export class RdfToQuadConverter extends TypedRepresentationConverter { +export class RdfToQuadConverter extends BaseTypedRepresentationConverter { public constructor() { super(rdfParser.getContentTypesPrioritized(), INTERNAL_QUADS); } diff --git a/src/storage/conversion/TypedRepresentationConverter.ts b/src/storage/conversion/TypedRepresentationConverter.ts index 75eb7e00f..60e41b31d 100644 --- a/src/storage/conversion/TypedRepresentationConverter.ts +++ b/src/storage/conversion/TypedRepresentationConverter.ts @@ -1,76 +1,17 @@ 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 | Promise; -type ValuePreferencesArg = - PromiseOrValue | - PromiseOrValue | - PromiseOrValue; - -async function toValuePreferences(arg: ValuePreferencesArg): Promise { - 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 TypedRepresentationConverter extends RepresentationConverter { - protected inputTypes: Promise; - protected outputTypes: Promise; - - 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 { - return this.inputTypes; - } + public abstract getInputTypes(): Promise; /** * Gets the supported output content types for this converter, mapped to a numerical quality. */ - public async getOutputTypes(): Promise { - 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 { - 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)}.`, - ); - } - } + public abstract getOutputTypes(): Promise; } diff --git a/test/integration/GuardedStream.test.ts b/test/integration/GuardedStream.test.ts index d1f478de7..1cadb4906 100644 --- a/test/integration/GuardedStream.test.ts +++ b/test/integration/GuardedStream.test.ts @@ -1,6 +1,5 @@ import { RepresentationMetadata, - TypedRepresentationConverter, readableToString, ChainedConverter, guardedStreamFrom, @@ -12,6 +11,7 @@ import { import type { Representation, RepresentationConverterArgs, Logger } from '../../src'; +import { BaseTypedRepresentationConverter } from '../../src/storage/conversion/BaseTypedRepresentationConverter'; jest.mock('../../src/logging/LogUtil', (): any => { const logger: Logger = @@ -20,7 +20,7 @@ jest.mock('../../src/logging/LogUtil', (): any => { }); const logger: jest.Mocked = getLoggerFor('GuardedStream') as any; -class DummyConverter extends TypedRepresentationConverter { +class DummyConverter extends BaseTypedRepresentationConverter { public constructor() { super('*/*', 'custom/type'); } diff --git a/test/unit/storage/conversion/TypedRepresentationConverter.test.ts b/test/unit/storage/conversion/BaseTypedRepresentationConverter.test.ts similarity index 92% rename from test/unit/storage/conversion/TypedRepresentationConverter.test.ts rename to test/unit/storage/conversion/BaseTypedRepresentationConverter.test.ts index f9d5561a3..dc82d1664 100644 --- a/test/unit/storage/conversion/TypedRepresentationConverter.test.ts +++ b/test/unit/storage/conversion/BaseTypedRepresentationConverter.test.ts @@ -1,12 +1,12 @@ +import { BaseTypedRepresentationConverter } from '../../../../src/storage/conversion/BaseTypedRepresentationConverter'; import type { RepresentationConverterArgs } from '../../../../src/storage/conversion/RepresentationConverter'; -import { TypedRepresentationConverter } from '../../../../src/storage/conversion/TypedRepresentationConverter'; import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; -class CustomTypedRepresentationConverter extends TypedRepresentationConverter { +class CustomTypedRepresentationConverter extends BaseTypedRepresentationConverter { public handle = jest.fn(); } -describe('A TypedRepresentationConverter', (): void => { +describe('A BaseTypedRepresentationConverter', (): void => { it('defaults to allowing everything.', async(): Promise => { const converter = new CustomTypedRepresentationConverter(); await expect(converter.getInputTypes()).resolves.toEqual({ diff --git a/test/unit/storage/conversion/ChainedConverter.test.ts b/test/unit/storage/conversion/ChainedConverter.test.ts index fa7d35c04..8c98cf990 100644 --- a/test/unit/storage/conversion/ChainedConverter.test.ts +++ b/test/unit/storage/conversion/ChainedConverter.test.ts @@ -4,13 +4,13 @@ import type { RepresentationPreferences, ValuePreferences, } from '../../../../src/http/representation/RepresentationPreferences'; +import { BaseTypedRepresentationConverter } from '../../../../src/storage/conversion/BaseTypedRepresentationConverter'; import { ChainedConverter } from '../../../../src/storage/conversion/ChainedConverter'; import { matchesMediaType } from '../../../../src/storage/conversion/ConversionUtil'; import type { RepresentationConverterArgs } from '../../../../src/storage/conversion/RepresentationConverter'; -import { TypedRepresentationConverter } from '../../../../src/storage/conversion/TypedRepresentationConverter'; import { CONTENT_TYPE } from '../../../../src/util/Vocabularies'; -class DummyConverter extends TypedRepresentationConverter { +class DummyConverter extends BaseTypedRepresentationConverter { private readonly inTypes: ValuePreferences; private readonly outTypes: ValuePreferences;