Skip to content

Commit

Permalink
Qute: introduce InjectableFragment
Browse files Browse the repository at this point in the history
- type-safe fragments should honor the selected variant
- fixes quarkiverse/quarkus-renarde#165
  • Loading branch information
mkouba committed Sep 11, 2023
1 parent bf87cc3 commit a35aa5e
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateGlobal;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Variant;
import io.quarkus.test.QuarkusUnitTest;

public class CheckedTemplateFragmentTest {
Expand All @@ -26,10 +27,21 @@ public class CheckedTemplateFragmentTest {
"{#fragment id=foo}{#for i in bar}{i_count}. <{i}>{#if i_hasNext}, {/if}{/for}{/fragment}"),
"templates/CheckedTemplateFragmentTest/foos.html"));

@SuppressWarnings("unchecked")
@Test
public void testFragment() {
assertEquals("Foo", Templates.items(null).getFragment("item").data("it", new Item("Foo")).render());
assertEquals("Foo", Templates.items$item(new Item("Foo")).render());
TemplateInstance items = Templates.items(null);
List<Variant> variants = (List<Variant>) items.getAttribute(TemplateInstance.VARIANTS);
assertEquals(1, variants.size());
assertEquals("text/html", variants.get(0).getContentType());
assertEquals("Foo", items.getFragment("item").data("it", new Item("Foo")).render());

TemplateInstance fragment = Templates.items$item(new Item("Foo"));
variants = (List<Variant>) fragment.getAttribute(TemplateInstance.VARIANTS);
assertEquals(1, variants.size());
assertEquals("text/html", variants.get(0).getContentType());
assertEquals("Foo", fragment.render());

assertEquals("FooAndBar is a long name", Templates.items$item(new Item("FooAndBar")).render());
assertEquals("1. <1>, 2. <2>, 3. <3>, 4. <4>, 5. <5>", Templates.foos$foo().render());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.quarkus.qute.deployment.typesafe.fragment;

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

import java.util.List;

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

import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Variant;
import io.quarkus.test.QuarkusUnitTest;

public class CheckedTemplateFragmentVariantTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addClasses(Templates.class, Item.class)
.addAsResource(new StringAsset(
"{#each items}{#fragment id='item'}<p>{it.name}</p>{/fragment}{/each}"),
"templates/CheckedTemplateFragmentVariantTest/items.html")
.addAsResource(new StringAsset(
"{#each items}{#fragment id='item'}{it.name}{/fragment}{/each}"),
"templates/CheckedTemplateFragmentVariantTest/items.txt"));

@SuppressWarnings("unchecked")
@Test
public void testFragment() {
TemplateInstance fragment = Templates.items$item(new Item("Foo"));
List<Variant> variants = (List<Variant>) fragment.getAttribute(TemplateInstance.VARIANTS);
assertEquals(2, variants.size());

assertEquals("<p>Foo</p>",
fragment.setAttribute(TemplateInstance.SELECTED_VARIANT, Variant.forContentType("text/html")).render());
assertEquals("Foo",
fragment.setAttribute(TemplateInstance.SELECTED_VARIANT, Variant.forContentType("text/plain")).render());
// A variant for application/json does not exist, use the default - html wins
assertEquals("<p>Foo</p>",
fragment.setAttribute(TemplateInstance.SELECTED_VARIANT, Variant.forContentType("application/json")).render());
}

@CheckedTemplate
public static class Templates {

static native TemplateInstance items(List<Item> items);

static native TemplateInstance items$item(Item it);

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,6 @@ Template getTemplate(InjectionPoint injectionPoint) {
if (path == null || path.isEmpty()) {
throw new IllegalStateException("No template location specified");
}
// We inject a delegating template in order to:
// 1. Be able to select an appropriate variant if needed
// 2. Be able to reload the template when needed, i.e. when the cache is cleared
return new InjectableTemplate(path, templateVariants, engine);
}

Expand All @@ -100,11 +97,18 @@ public Template getInjectableTemplate(String path) {
return new InjectableTemplate(path, templateVariants, engine);
}

/**
* We inject a delegating template in order to:
*
* 1. Be able to select an appropriate variant if needed
* 2. Be able to reload the template when needed, i.e. when the cache is cleared
*/
static class InjectableTemplate implements Template {

private final String path;
private final TemplateVariants variants;
private final Engine engine;
// Some methods may only work if a single template variant is found
private final LazyValue<Template> unambiguousTemplate;

public InjectableTemplate(String path, Map<String, TemplateVariants> templateVariants, Engine engine) {
Expand Down Expand Up @@ -179,10 +183,7 @@ public String getId() {

@Override
public Fragment getFragment(String identifier) {
if (unambiguousTemplate != null) {
return unambiguousTemplate.get().getFragment(identifier);
}
throw ambiguousTemplates("getFragment()");
return new InjectableFragment(identifier);
}

@Override
Expand All @@ -202,6 +203,66 @@ public String toString() {
return "Injectable template [path=" + path + "]";
}

class InjectableFragment implements Fragment {

private final String identifier;

InjectableFragment(String identifier) {
this.identifier = identifier;
}

@Override
public List<Expression> getExpressions() {
return InjectableTemplate.this.getExpressions();
}

@Override
public Expression findExpression(Predicate<Expression> predicate) {
return InjectableTemplate.this.findExpression(predicate);
}

@Override
public String getGeneratedId() {
return InjectableTemplate.this.getGeneratedId();
}

@Override
public Optional<Variant> getVariant() {
return InjectableTemplate.this.getVariant();
}

@Override
public List<ParameterDeclaration> getParameterDeclarations() {
return InjectableTemplate.this.getParameterDeclarations();
}

@Override
public String getId() {
return identifier;
}

@Override
public Template getOriginalTemplate() {
return InjectableTemplate.this;
}

@Override
public Fragment getFragment(String id) {
return InjectableTemplate.this.getFragment(id);
}

@Override
public Set<String> getFragmentIds() {
return InjectableTemplate.this.getFragmentIds();
}

@Override
public TemplateInstance instance() {
return new InjectableFragmentTemplateInstanceImpl(identifier);
}

}

class InjectableTemplateInstanceImpl extends TemplateInstanceBase {

InjectableTemplateInstanceImpl() {
Expand Down Expand Up @@ -261,7 +322,7 @@ private TemplateInstance templateInstance() {
return instance;
}

private Template template() {
protected Template template() {
if (unambiguousTemplate != null) {
return unambiguousTemplate.get();
}
Expand All @@ -280,6 +341,22 @@ private Template template() {
}

}

class InjectableFragmentTemplateInstanceImpl extends InjectableTemplateInstanceImpl {

private final String identifier;

private InjectableFragmentTemplateInstanceImpl(String identifier) {
this.identifier = identifier;
}

@Override
protected Template template() {
return super.template().getFragment(identifier);
}

}

}

static class TemplateVariants {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,14 @@ default String render() {
}

/**
* If invoked upon a fragment instance then delegate to the defining template.
*
* @return an immutable list of expressions used in the template
*/
List<Expression> getExpressions();

/**
* If invoked upon a fragment instance then delegate to the defining template.
*
* @param predicate
* @return the first expression matching the given predicate or {@code null} if no such expression is used in the template
Expand All @@ -146,12 +148,14 @@ default String render() {
String getId();

/**
* If invoked upon a fragment instance then delegate to the defining template.
*
* @return the template variant
*/
Optional<Variant> getVariant();

/**
* If invoked upon a fragment instance then delegate to the defining template.
*
* @return an immutable list of all parameter declarations defined in the template
*/
Expand Down

0 comments on commit a35aa5e

Please sign in to comment.