Skip to content

Commit

Permalink
Qute: template records
Browse files Browse the repository at this point in the history
- an alternative syntax for type-safe templates
  • Loading branch information
mkouba committed Jul 14, 2023
1 parent 65ca2db commit 4dbe592
Show file tree
Hide file tree
Showing 27 changed files with 878 additions and 102 deletions.
54 changes: 52 additions & 2 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1751,7 +1751,18 @@ IMPORTANT: The type of a default value is not validated in <<standalone,Qute sta

[[typesafe_templates]]
=== Type-safe Templates
You can also define type-safe templates in your Java code.

You can define type-safe templates in your Java code.
Parameters of type-safe templates are automatically turned into _parameter declarations_ that are used to bind <<typesafe_expressions>>.
The type-safe expressions are then validated at build time.

There are two ways to define a type-safe template:

1. Annotate a class with `@io.quarkus.qute.CheckedTemplate` and all its `static native` methods will be used to define type-safe templates and the list of parameters they require.
2. Use a Java record that implements `io.quarkus.qute.TemplateInstance`; the record components represent the template parameters and `@io.quarkus.qute.CheckedTemplate` can be optionally used to configure the template.

==== Nested Type-safe Templates

If using <<resteasy_integration,templates in Jakarta REST resources>>, you can rely on the following convention:

- Organise your template files in the `/src/main/resources/templates` directory, by grouping them into one directory per resource class. So, if
Expand Down Expand Up @@ -1846,6 +1857,41 @@ public class HelloResource {
}
----

==== Template Records

A Java record that implements `io.quarkus.qute.TemplateInstance` denotes a type-safe template.
The record components represent the template parameters and `@io.quarkus.qute.CheckedTemplate` can be optionally used to configure the template.

.HelloResource.java
[source,java]
----
package org.acme.quarkus.sample;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import io.quarkus.qute.TemplateInstance;
@Path("hello")
public class HelloResource {
record Hello(String name) implements TemplateInstance {} <1>
@GET
@Produces(MediaType.TEXT_PLAIN)
public TemplateInstance get(@QueryParam("name") String name) {
return new Hello(name); <2>
}
}
----
<1> Declares a type-safe template with the Java record.
<2> Instantiate the record and use it as an ordinary `TemplateInstance`.


==== Customized Template Path

The template path of a `@CheckedTemplate` method consists of the _base path_ and a _defaulted name_.
Expand Down Expand Up @@ -2793,10 +2839,14 @@ Engine::
* First, no managed `Engine` instance is available out of the box.
You'll need to configure a new instance via `Engine.builder()`.

Templates::
Template locators::
* By default, no <<template-locator,template locators>> are registered, i.e. `Engine.getTemplate(String)` will not work.
* You can register a custom template locator using `EngineBuilder.addLocator()` or parse a template manually and put the result in the cache via `Engine.putTemplate(String, Template)`.

Template initializers::
* No `TemplateInstance.Initializer` is registered by default, therefore <<global_variables,`@TemplateGlobal`>> annotations are ignored.
* A custom `TemplateInstance.Initializer` can be registered with `EngineBuilder#addTemplateInstanceInitializer()` and initialize a template instance with any data and attributes.

Sections::
* No section helpers are registered by default.
* The default set of value resolvers can be registered via the convenient `EngineBuilder.addDefaultSectionHelpers()` method and the `EngineBuilder.addDefaults()` method respectively.
Expand Down
37 changes: 37 additions & 0 deletions extensions/mailer/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,42 @@
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>java-17</id>
<activation>
<jdk>[17,)</jdk>
</activation>

<properties>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-java17-directory</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>src/test/java17</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class MailTemplateLocationTest {
MailTemplates mailTemplates;

@Test
public void testValidationFailed() {
public void testLocation() {
mailTemplates.send().await().atMost(Duration.ofSeconds(5));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.quarkus.mailer;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.mailer.MailTemplate.MailTemplateInstance;
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.test.QuarkusUnitTest;
import io.vertx.ext.mail.MailMessage;
import jakarta.inject.Inject;

public class MailTemplateRecordTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addClasses(confirmation.class)
.addAsResource("mock-config.properties", "application.properties")
.addAsResource(new StringAsset(""
+ "<html>{name}</html>"), "templates/confirmation.html"));

@Inject
MockMailbox mockMailbox;

@Test
public void testMailTemplateRecord() {
new confirmation("Ondrej").to("[email protected]").from("[email protected]").subject("test mailer")
.send().await().indefinitely();
assertEquals(1, mockMailbox.getMailMessagesSentTo("[email protected]").size());
MailMessage message = mockMailbox.getMailMessagesSentTo("[email protected]").get(0);
assertEquals("[email protected]", message.getFrom());
assertEquals("<html>Ondrej</html>", message.getHtml());
}

@CheckedTemplate(basePath = "")
record confirmation(String name) implements MailTemplateInstance {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,31 +36,57 @@ default MailTemplateInstance data(String key, Object value) {
*/
interface MailTemplateInstance {

MailTemplateInstance mail(Mail mail);
default MailTemplateInstance mail(Mail mail) {
throw new UnsupportedOperationException();
}

MailTemplateInstance to(String... to);
default MailTemplateInstance to(String... to) {
throw new UnsupportedOperationException();
}

MailTemplateInstance cc(String... cc);
default MailTemplateInstance cc(String... cc) {
throw new UnsupportedOperationException();
}

MailTemplateInstance bcc(String... bcc);
default MailTemplateInstance bcc(String... bcc) {
throw new UnsupportedOperationException();
}

MailTemplateInstance subject(String subject);
default MailTemplateInstance subject(String subject) {
throw new UnsupportedOperationException();
}

MailTemplateInstance from(String from);
default MailTemplateInstance from(String from) {
throw new UnsupportedOperationException();
}

MailTemplateInstance replyTo(String replyTo);
default MailTemplateInstance replyTo(String replyTo) {
throw new UnsupportedOperationException();
}

MailTemplateInstance replyTo(String... replyTo);
default MailTemplateInstance replyTo(String... replyTo) {
throw new UnsupportedOperationException();
}

MailTemplateInstance bounceAddress(String bounceAddress);
default MailTemplateInstance bounceAddress(String bounceAddress) {
throw new UnsupportedOperationException();
}

MailTemplateInstance addInlineAttachment(String name, File file, String contentType, String contentId);
default MailTemplateInstance addInlineAttachment(String name, File file, String contentType, String contentId) {
throw new UnsupportedOperationException();
}

MailTemplateInstance addInlineAttachment(String name, byte[] data, String contentType, String contentId);
default MailTemplateInstance addInlineAttachment(String name, byte[] data, String contentType, String contentId) {
throw new UnsupportedOperationException();
}

MailTemplateInstance addAttachment(String name, File file, String contentType);
default MailTemplateInstance addAttachment(String name, File file, String contentType) {
throw new UnsupportedOperationException();
}

MailTemplateInstance addAttachment(String name, byte[] data, String contentType);
default MailTemplateInstance addAttachment(String name, byte[] data, String contentType) {
throw new UnsupportedOperationException();
}

/**
*
Expand All @@ -69,7 +95,9 @@ interface MailTemplateInstance {
* @return self
* @see io.quarkus.qute.TemplateInstance#data(String, Object)
*/
MailTemplateInstance data(String key, Object value);
default MailTemplateInstance data(String key, Object value) {
throw new UnsupportedOperationException();
}

/**
*
Expand All @@ -78,7 +106,9 @@ interface MailTemplateInstance {
* @return self
* @see io.quarkus.qute.TemplateInstance#setAttribute(String, Object)
*/
MailTemplateInstance setAttribute(String key, Object value);
default MailTemplateInstance setAttribute(String key, Object value) {
throw new UnsupportedOperationException();
}

/**
* Sends all e-mail definitions based on available template variants, i.e. {@code text/html} and {@code text/plain}
Expand All @@ -87,7 +117,9 @@ interface MailTemplateInstance {
* @return a {@link Uni} indicating when the mails have been sent
* @see ReactiveMailer#send(Mail...)
*/
Uni<Void> send();
default Uni<Void> send() {
throw new UnsupportedOperationException();
}

/**
* The returned instance does not represent a specific template but a delegating template.
Expand All @@ -97,7 +129,9 @@ interface MailTemplateInstance {
*
* @return the underlying template instance
*/
TemplateInstance templateInstance();
default TemplateInstance templateInstance() {
throw new UnsupportedOperationException();
}

}

Expand Down
42 changes: 40 additions & 2 deletions extensions/qute/deployment/pom.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>quarkus-qute-parent</artifactId>
Expand Down Expand Up @@ -90,4 +90,42 @@
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>java-17</id>
<activation>
<jdk>[17,)</jdk>
</activation>

<properties>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-java17-directory</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>src/test/java17</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,21 @@

import java.util.List;

import org.jboss.jandex.MethodInfo;

import io.quarkus.builder.item.MultiBuildItem;
import io.quarkus.qute.Expression;

final class CheckedFragmentValidationBuildItem extends MultiBuildItem {

final String templateGeneratedId;
final String templateId;
final String fragmentId;
final List<Expression> fragmentExpressions;
final MethodInfo method;
final CheckedTemplateBuildItem checkedTemplate;

public CheckedFragmentValidationBuildItem(String templateGeneratedId, String templateId, String fragmentId,
public CheckedFragmentValidationBuildItem(String templateGeneratedId,
List<Expression> fragmentExpressions,
MethodInfo method) {
CheckedTemplateBuildItem checkedTemplate) {
this.templateGeneratedId = templateGeneratedId;
this.templateId = templateId;
this.fragmentId = fragmentId;
this.fragmentExpressions = fragmentExpressions;
this.method = method;
this.checkedTemplate = checkedTemplate;
}

}
Loading

0 comments on commit 4dbe592

Please sign in to comment.