Skip to content

Commit

Permalink
fix: Convert TemplateEngine to AsyncHandlers
Browse files Browse the repository at this point in the history
  • Loading branch information
wkerckho authored and joachimvh committed Sep 26, 2022
1 parent a3c7baf commit cf74ce3
Show file tree
Hide file tree
Showing 43 changed files with 479 additions and 316 deletions.
5 changes: 4 additions & 1 deletion RELEASE_NOTES.md
Expand Up @@ -23,7 +23,10 @@ The following changes pertain to the imports in the default configs:

The following changes are relevant for v5 custom configs that replaced certain features.

- ...
- Updated template configs.
- `/app/main/general/templates.json` was added to configure a generic template engine handler.
- `/app/main/default.json` now imports the above config file.
- All files configuring template engines.

### Interface changes

Expand Down
5 changes: 1 addition & 4 deletions config/app/init/initializers/prefilled-root.json
Expand Up @@ -17,10 +17,7 @@
"@type": "TemplatedResourcesGenerator",
"templateFolder": "@css:templates/root/prefilled",
"factory": { "@type": "ExtensionBasedMapperFactory" },
"templateEngine": {
"@type": "HandlebarsTemplateEngine",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
},
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"metadataStrategy": { "@id": "urn:solid-server:default:MetadataStrategy" },
"store": { "@id": "urn:solid-server:default:ResourceStore"}
},
Expand Down
5 changes: 1 addition & 4 deletions config/app/init/initializers/root.json
Expand Up @@ -17,10 +17,7 @@
"@type": "TemplatedResourcesGenerator",
"templateFolder": "@css:templates/root/empty",
"factory": { "@type": "ExtensionBasedMapperFactory" },
"templateEngine": {
"@type": "HandlebarsTemplateEngine",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
},
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"metadataStrategy": { "@id": "urn:solid-server:default:MetadataStrategy" },
"store": { "@id": "urn:solid-server:default:ResourceStore"}
},
Expand Down
3 changes: 3 additions & 0 deletions config/app/main/default.json
@@ -1,5 +1,8 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
"import": [
"css:config/app/main/general/templates.json"
],
"@graph": [
{
"comment": "This is the entry point to the application. It can be used to both start and stop the server.",
Expand Down
22 changes: 22 additions & 0 deletions config/app/main/general/templates.json
@@ -0,0 +1,22 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
"@graph": [
{
"comment": "Template engine that finds the appropriate template engine to use based on the template extension.",
"@id": "urn:solid-server:default:TemplateEngine",
"@type": "WaterfallHandler",
"handlers": [
{
"comment": "Template engine that supports EJS templates.",
"@type": "EjsTemplateEngine",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
},
{
"comment": "Template engine that supports Handlebars (HBS) templates",
"@type": "HandlebarsTemplateEngine",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
}
]
}
]
}
17 changes: 7 additions & 10 deletions config/app/setup/handlers/setup.json
Expand Up @@ -26,15 +26,15 @@
"engines": [
{
"comment": "Renders the main setup template.",
"@type": "EjsTemplateEngine",
"template": "@css:templates/setup/index.html.ejs",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
"@type": "StaticTemplateEngine",
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"template": "@css:templates/setup/index.html.ejs"
},
{
"comment": "Will embed the result of the first engine into the main HTML template.",
"@type": "EjsTemplateEngine",
"template": "@css:templates/main.html.ejs",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
"@type": "StaticTemplateEngine",
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"template": "@css:templates/main.html.ejs"
}
]
}
Expand Down Expand Up @@ -62,10 +62,7 @@
"@type": "TemplatedResourcesGenerator",
"templateFolder": "@css:templates/root/empty",
"factory": { "@type": "ExtensionBasedMapperFactory" },
"templateEngine": {
"@type": "HandlebarsTemplateEngine",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
},
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"metadataStrategy": { "@id": "urn:solid-server:default:MetadataStrategy" },
"store": { "@id": "urn:solid-server:default:ResourceStore"}
},
Expand Down
5 changes: 1 addition & 4 deletions config/identity/access/initializers/idp.json
Expand Up @@ -17,10 +17,7 @@
"@type": "TemplatedResourcesGenerator",
"templateFolder": "@css:templates/root/empty",
"factory": { "@type": "ExtensionBasedMapperFactory" },
"templateEngine": {
"@type": "HandlebarsTemplateEngine",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
},
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"metadataStrategy": { "@id": "urn:solid-server:default:MetadataStrategy" },
"store": { "@id": "urn:solid-server:default:ResourceStore"}
},
Expand Down
5 changes: 1 addition & 4 deletions config/identity/access/initializers/well-known.json
Expand Up @@ -17,10 +17,7 @@
"@type": "TemplatedResourcesGenerator",
"templateFolder": "@css:templates/root/empty",
"factory": { "@type": "ExtensionBasedMapperFactory" },
"templateEngine": {
"@type": "HandlebarsTemplateEngine",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
},
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"metadataStrategy": { "@id": "urn:solid-server:default:MetadataStrategy" },
"store": { "@id": "urn:solid-server:default:ResourceStore"}
},
Expand Down
Expand Up @@ -16,9 +16,9 @@
"@type": "ForgotPasswordHandler",
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
"args_templateEngine": {
"@type": "EjsTemplateEngine",
"template": "@css:templates/identity/email-password/reset-password-email.html.ejs",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
"@type": "StaticTemplateEngine",
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"template": "@css:templates/identity/email-password/reset-password-email.html.ejs"
},
"args_emailSender": { "@id": "urn:solid-server:default:EmailSender" },
"args_resetRoute": { "@id": "urn:solid-server:auth:password:ResetPasswordRoute" }
Expand Down
9 changes: 4 additions & 5 deletions config/identity/handler/interaction/views/html.json
Expand Up @@ -12,14 +12,13 @@
"engines": [
{
"comment": "Will be called with specific templates to generate HTML snippets.",
"@type": "EjsTemplateEngine",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
"@id": "urn:solid-server:default:TemplateEngine"
},
{
"comment": "Will embed the result of the first engine into the main HTML template.",
"@type": "EjsTemplateEngine",
"template": "@css:templates/main.html.ejs",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
"@type": "StaticTemplateEngine",
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"template": "@css:templates/main.html.ejs"
}
]
},
Expand Down
5 changes: 1 addition & 4 deletions config/identity/pod/resource-generators/templated.json
Expand Up @@ -9,10 +9,7 @@
"factory": {
"@type": "ExtensionBasedMapperFactory"
},
"templateEngine": {
"@type": "HandlebarsTemplateEngine",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
},
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"metadataStrategy": { "@id": "urn:solid-server:default:MetadataStrategy" },
"store": { "@id": "urn:solid-server:default:ResourceStore"}
}
Expand Down
Expand Up @@ -12,14 +12,13 @@
"engines": [
{
"comment": "Will be called with specific templates to generate HTML snippets.",
"@type": "EjsTemplateEngine",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
"@id": "urn:solid-server:default:TemplateEngine"
},
{
"comment": "Will embed the result of the first engine into the main HTML template.",
"@type": "EjsTemplateEngine",
"template": "@css:templates/main.html.ejs",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
"@type": "StaticTemplateEngine",
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"template": "@css:templates/main.html.ejs"
}
]
}
Expand Down
5 changes: 1 addition & 4 deletions config/util/representation-conversion/converters/errors.json
Expand Up @@ -13,10 +13,7 @@
"comment": "Converts an error into a Markdown description of its details.",
"@id": "urn:solid-server:default:ErrorToTemplateConverter",
"@type": "ErrorToTemplateConverter",
"templateEngine": {
"@type": "HandlebarsTemplateEngine",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
}
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" }
}
]
}
12 changes: 6 additions & 6 deletions config/util/representation-conversion/converters/markdown.json
Expand Up @@ -6,19 +6,19 @@
"@id": "urn:solid-server:default:MarkdownToHtmlConverter",
"@type": "MarkdownToHtmlConverter",
"templateEngine": {
"@type": "EjsTemplateEngine",
"template": "@css:templates/main.html.ejs",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
"@type": "StaticTemplateEngine",
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"template": "@css:templates/main.html.ejs"
}
},
{
"comment": "Converts a container into a Markdown listing of its contents.",
"@id": "urn:solid-server:default:ContainerToTemplateConverter",
"@type": "ContainerToTemplateConverter",
"templateEngine": {
"@type": "HandlebarsTemplateEngine",
"template": "@css:templates/container.md.hbs",
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
"@type": "StaticTemplateEngine",
"templateEngine": { "@id": "urn:solid-server:default:TemplateEngine" },
"template": "@css:templates/container.md.hbs"
},
"contentType": "text/markdown",
"identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" }
Expand Down
2 changes: 1 addition & 1 deletion src/identity/interaction/HtmlViewHandler.ts
Expand Up @@ -55,7 +55,7 @@ export class HtmlViewHandler extends InteractionHandler {
public async handle({ operation, oidcInteraction }: InteractionHandlerInput): Promise<Representation> {
const template = this.templates[operation.target.path];
const contents = { idpIndex: this.idpIndex, authenticating: Boolean(oidcInteraction) };
const result = await this.templateEngine.render(contents, { templateFile: template });
const result = await this.templateEngine.handleSafe({ contents, template: { templateFile: template }});
return new BasicRepresentation(result, operation.target, TEXT_HTML);
}
}
Expand Up @@ -75,7 +75,7 @@ export class ForgotPasswordHandler extends BaseInteractionHandler {
private async sendResetMail(recordId: string, email: string): Promise<void> {
this.logger.info(`Sending password reset to ${email}`);
const resetLink = `${this.resetRoute.getPath()}?rid=${encodeURIComponent(recordId)}`;
const renderedEmail = await this.templateEngine.render({ resetLink });
const renderedEmail = await this.templateEngine.handleSafe({ contents: { resetLink }});
await this.emailSender.handleSafe({
recipient: email,
subject: 'Reset your password',
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Expand Up @@ -453,8 +453,11 @@ export * from './util/map/WrappedSetMultiMap';
// Util/Templates
export * from './util/templates/ChainedTemplateEngine';
export * from './util/templates/EjsTemplateEngine';
export * from './util/templates/ExtensionBasedTemplateEngine';
export * from './util/templates/HandlebarsTemplateEngine';
export * from './util/templates/StaticTemplateEngine';
export * from './util/templates/TemplateEngine';
export * from './util/templates/TemplateUtil';

// Util
export * from './util/ContentTypes';
Expand Down
2 changes: 1 addition & 1 deletion src/init/setup/SetupHttpHandler.ts
Expand Up @@ -76,7 +76,7 @@ export class SetupHttpHandler extends OperationHttpHandler {
* Returns the HTML representation of the setup page.
*/
private async handleGet(operation: Operation): Promise<ResponseDescription> {
const result = await this.templateEngine.render({});
const result = await this.templateEngine.handleSafe({ contents: {}});
const representation = new BasicRepresentation(result, operation.target, TEXT_HTML);
return new OkResponseDescription(representation.metadata, representation.data);
}
Expand Down
4 changes: 2 additions & 2 deletions src/pods/generate/TemplatedResourcesGenerator.ts
Expand Up @@ -206,9 +206,9 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
/**
* Creates a read stream from the file and applies the template if necessary.
*/
private async processFile(link: TemplateResourceLink, options: Dict<string>): Promise<Guarded<Readable>> {
private async processFile(link: TemplateResourceLink, contents: Dict<string>): Promise<Guarded<Readable>> {
if (link.isTemplate) {
const rendered = await this.templateEngine.render(options, { templateFile: link.filePath });
const rendered = await this.templateEngine.handleSafe({ contents, template: { templateFile: link.filePath }});
return guardedStreamFrom(rendered);
}
return guardStream(createReadStream(link.filePath));
Expand Down
4 changes: 2 additions & 2 deletions src/storage/conversion/ContainerToTemplateConverter.ts
Expand Up @@ -43,13 +43,13 @@ export class ContainerToTemplateConverter extends BaseTypedRepresentationConvert
}

public async handle({ identifier, representation }: RepresentationConverterArgs): Promise<Representation> {
const rendered = await this.templateEngine.render({
const rendered = await this.templateEngine.handleSafe({ contents: {
identifier: identifier.path,
name: this.getLocalName(identifier.path),
container: true,
children: await this.getChildResources(identifier, representation.data),
parents: this.getParentContainers(identifier),
});
}});
return new BasicRepresentation(rendered, representation.metadata, this.contentType);
}

Expand Down
4 changes: 2 additions & 2 deletions src/storage/conversion/DynamicJsonToTemplateConverter.ts
Expand Up @@ -58,9 +58,9 @@ export class DynamicJsonToTemplateConverter extends RepresentationConverter {
return representation;
}

const json = JSON.parse(await readableToString(representation.data));
const contents = JSON.parse(await readableToString(representation.data));

const rendered = await this.templateEngine.render(json, { templateFile: typeMap[type] });
const rendered = await this.templateEngine.handleSafe({ contents, template: { templateFile: typeMap[type] }});
const metadata = new RepresentationMetadata(representation.metadata, { [CONTENT_TYPE]: type });

return new BasicRepresentation(rendered, metadata);
Expand Down
9 changes: 5 additions & 4 deletions src/storage/conversion/ErrorToTemplateConverter.ts
Expand Up @@ -65,17 +65,18 @@ export class ErrorToTemplateConverter extends BaseTypedRepresentationConverter {
try {
const templateFile = `${error.errorCode}${this.extension}`;
assert(isValidFileName(templateFile), 'Invalid error template name');
description = await this.templateEngine.render(error.details ?? {},
{ templateFile, templatePath: this.codeTemplatesPath });
description = await this.templateEngine.handleSafe({ contents: error.details ?? {},
template: { templateFile, templatePath: this.codeTemplatesPath }});
} catch {
// In case no template is found, or rendering errors, we still want to convert
}
}

// Render the main template, embedding the rendered error description
const { name, message, stack } = error;
const variables = { name, message, stack, description };
const rendered = await this.templateEngine.render(variables, { templateFile: this.mainTemplatePath });
const contents = { name, message, stack, description };
const rendered = await this.templateEngine
.handleSafe({ contents, template: { templateFile: this.mainTemplatePath }});

return new BasicRepresentation(rendered, representation.metadata, this.contentType);
}
Expand Down
2 changes: 1 addition & 1 deletion src/storage/conversion/MarkdownToHtmlConverter.ts
Expand Up @@ -24,7 +24,7 @@ export class MarkdownToHtmlConverter extends BaseTypedRepresentationConverter {
public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {
const markdown = await readableToString(representation.data);
const htmlBody = marked(markdown);
const html = await this.templateEngine.render({ htmlBody });
const html = await this.templateEngine.handleSafe({ contents: { htmlBody }});

return new BasicRepresentation(html, representation.metadata, TEXT_HTML);
}
Expand Down
18 changes: 11 additions & 7 deletions src/util/templates/ChainedTemplateEngine.ts
@@ -1,4 +1,5 @@
import type { Template, TemplateEngine } from './TemplateEngine';
import type { TemplateEngineInput } from './TemplateEngine';
import { TemplateEngine } from './TemplateEngine';
import Dict = NodeJS.Dict;

/**
Expand All @@ -9,7 +10,7 @@ import Dict = NodeJS.Dict;
* All subsequent engines will be called with no template parameter.
* Contents will still be passed along and another entry will be added for the body of the previous output.
*/
export class ChainedTemplateEngine<T extends Dict<any> = Dict<any>> implements TemplateEngine<T> {
export class ChainedTemplateEngine<T extends Dict<any> = Dict<any>> extends TemplateEngine<T> {
private readonly firstEngine: TemplateEngine<T>;
private readonly chainedEngines: TemplateEngine[];
private readonly renderedName: string;
Expand All @@ -19,6 +20,7 @@ export class ChainedTemplateEngine<T extends Dict<any> = Dict<any>> implements T
* @param renderedName - The name of the key used to pass the body of one engine to the next.
*/
public constructor(engines: TemplateEngine[], renderedName = 'body') {
super();
if (engines.length === 0) {
throw new Error('At least 1 engine needs to be provided.');
}
Expand All @@ -27,12 +29,14 @@ export class ChainedTemplateEngine<T extends Dict<any> = Dict<any>> implements T
this.renderedName = renderedName;
}

public async render(contents: T): Promise<string>;
public async render<TCustom = T>(contents: TCustom, template: Template): Promise<string>;
public async render<TCustom = T>(contents: TCustom, template?: Template): Promise<string> {
let body = await this.firstEngine.render(contents, template!);
public async canHandle(input: TemplateEngineInput<T>): Promise<void> {
return this.firstEngine.canHandle(input);
}

public async handle({ contents, template }: TemplateEngineInput<T>): Promise<string> {
let body = await this.firstEngine.handle({ contents, template });
for (const engine of this.chainedEngines) {
body = await engine.render({ ...contents, [this.renderedName]: body });
body = await engine.handleSafe({ contents: { ...contents, [this.renderedName]: body }});
}
return body;
}
Expand Down

0 comments on commit cf74ce3

Please sign in to comment.