feat: Support content negotiation for IDP requests

This commit is contained in:
Joachim Van Herwegen
2021-08-10 15:55:49 +02:00
parent 7b42c72142
commit 80ebd02cc4
20 changed files with 483 additions and 209 deletions

View File

@@ -0,0 +1,96 @@
import type { Term, NamedNode } from 'rdf-js';
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
import type { Representation } from '../../ldp/representation/Representation';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import type { ValuePreferences } from '../../ldp/representation/RepresentationPreferences';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import { APPLICATION_JSON } from '../../util/ContentTypes';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { readableToString } from '../../util/StreamUtil';
import type { TemplateEngine } from '../../util/templates/TemplateEngine';
import { CONTENT_TYPE, CONTENT_TYPE_TERM, SOLID_META } from '../../util/Vocabularies';
import { getConversionTarget } from './ConversionUtil';
import { RepresentationConverter } from './RepresentationConverter';
import type { RepresentationConverterArgs } from './RepresentationConverter';
/**
* Converts JSON data by using it as input parameters for rendering a template.
* The `extension` field can be used to only support a specific type of templates,
* such as ".ejs" for EJS templates.
*
* To find the templates it expects the Representation metadata to contain `SOLID_META.template` triples,
* with the objects being the template paths.
* For each of those templates there also needs to be a CONTENT_TYPE triple
* describing the content-type of that template.
*
* The output of the result depends on the content-type matched with the template.
*/
export class DynamicJsonToTemplateConverter extends RepresentationConverter {
private readonly templateEngine: TemplateEngine;
public constructor(templateEngine: TemplateEngine) {
super();
this.templateEngine = templateEngine;
}
public async canHandle(input: RepresentationConverterArgs): Promise<void> {
if (input.representation.metadata.contentType !== APPLICATION_JSON) {
throw new NotImplementedHttpError('Only JSON data is supported');
}
const { identifier, representation, preferences } = input;
// Can only handle this input if we can find a type to convert to
const typeMap = this.constructTypeMap(identifier, representation);
this.findType(typeMap, preferences.type);
}
public async handle(input: RepresentationConverterArgs): Promise<Representation> {
const { identifier, representation, preferences } = input;
const typeMap = this.constructTypeMap(identifier, representation);
const type = this.findType(typeMap, preferences.type);
const json = JSON.parse(await readableToString(representation.data));
const rendered = await this.templateEngine.render(json, { templateFile: typeMap[type] });
const metadata = new RepresentationMetadata(representation.metadata, { [CONTENT_TYPE]: type });
return new BasicRepresentation(rendered, metadata);
}
/**
* Uses the metadata of the Representation to create a map where each key is a content-type
* and each value is the path of the corresponding template.
*/
private constructTypeMap(identifier: ResourceIdentifier, representation: Representation): Record<string, string> {
// Finds the templates in the metadata
const templates: NamedNode[] = representation.metadata.quads(identifier.path, SOLID_META.terms.template)
.map((quad): Term => quad.object)
.filter((term: Term): boolean => term.termType === 'NamedNode') as NamedNode[];
// Maps all content-types to their template
const typeMap: Record<string, string> = {};
for (const template of templates) {
const types = representation.metadata.quads(template, CONTENT_TYPE_TERM).map((quad): string => quad.object.value);
for (const type of types) {
typeMap[type] = template.value;
}
}
return typeMap;
}
/**
* Finds the best content-type to convert to based on the provided templates and preferences.
*/
private findType(typeMap: Record<string, string>, typePreferences: ValuePreferences = {}): string {
const typeWeights = Object.fromEntries(Object.keys(typeMap).map((type: string): [ string, 1 ] => [ type, 1 ]));
const type = getConversionTarget(typeWeights, typePreferences);
if (!type) {
throw new NotImplementedHttpError(
`No templates found matching ${Object.keys(typePreferences)}, only ${Object.keys(typeMap)}`,
);
}
return type;
}
}