Skip to content

Commit

Permalink
Merge pull request #39959 from mkouba/issue-39832
Browse files Browse the repository at this point in the history
Qute: add global variables as computed data
  • Loading branch information
geoand authored Apr 9, 2024
2 parents d12aefc + 84788e6 commit 510e89e
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 13 deletions.
4 changes: 3 additions & 1 deletion docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2436,9 +2436,11 @@ The `io.quarkus.qute.TemplateGlobal` annotation can be used to denote static fie

Global variables are:

* added to the data map of any `TemplateInstance` during initialization,
* added as _computed data_ of any `TemplateInstance` during initialization,
* accessible with the `global:` namespace.

NOTE: When using `TemplateInstance#computedData(String, Function<String, Object>)` a mapping function is associated with a specific key and this function is used each time a value for the given key is requested. In case of global variables, a static method is called or a static field is read in the mapping function.

.Global Variables Definition
[source,java]
----
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package io.quarkus.qute.deployment.globals;

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

import java.util.concurrent.atomic.AtomicBoolean;

import jakarta.inject.Inject;

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

import io.quarkus.qute.Engine;
import io.quarkus.qute.Qute;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateGlobal;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.test.QuarkusUnitTest;

public class TemplateGlobalTest {
Expand All @@ -23,11 +29,19 @@ public class TemplateGlobalTest {
"Hello {currentUser}|{global:currentUser}! Your name is {_name}|{global:_name}. You're {age}|{global:age} years old."),
"templates/hello.txt"));

@Inject
Engine engine;

@Inject
Template hello;

@Test
public void testTemplateData() {
TemplateInstance instance = engine.parse("Hello {age}!").instance();
assertFalse(Globals.AGE_USED.get());
assertEquals("Hello 40!", instance.render());
assertTrue(Globals.AGE_USED.get());

assertEquals("Hello Fu|Fu! Your name is Lu|Lu. You're 40|40 years old.", hello.render());
assertEquals("Hello Fu|Fu! Your name is Lu|Lu. You're 40|40 years old.",
Qute.fmt(
Expand All @@ -45,11 +59,14 @@ public void testTemplateData() {

public static class Globals {

static final AtomicBoolean AGE_USED = new AtomicBoolean();

@TemplateGlobal(name = "currentUser")
static String user = "Fu";

@TemplateGlobal
static int age() {
AGE_USED.set(true);
return user.equals("Fu") ? 40 : 20;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,8 @@ public Template getTemplate() {
private TemplateInstance templateInstance() {
TemplateInstance instance = template().instance();
if (dataMap != null) {
dataMap.forEach(instance::data);
dataMap.forEachData(instance::data);
dataMap.forEachComputedData(instance::computedData);
} else if (data != null) {
instance.data(data);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.function.Function;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
Expand Down Expand Up @@ -32,7 +33,7 @@ public interface TemplateInstance {

/**
* Set the the root data object. Invocation of this method removes any data set previously by
* {@link #data(String, Object)}.
* {@link #data(String, Object)} and {@link #computedData(String, Function)}.
*
* @param data
* @return
Expand All @@ -42,8 +43,8 @@ default TemplateInstance data(Object data) {
}

/**
* Put the data in a map. The map will be used as the root context object during rendering. Invocation of this
* method removes the root data object previously set by {@link #data(Object)}.
* Put the data in a map. The map will be used as the root context object during rendering. Remove the root data object
* previously set by {@link #data(Object)}.
*
* @param key
* @param data
Expand All @@ -53,6 +54,21 @@ default TemplateInstance data(String key, Object data) {
throw new UnsupportedOperationException();
}

/**
* Associates the specified mapping function with the specified key. The function is applied each time a value for the given
* key is requested. Also removes the root data object previously set by {@link #data(Object)}.
* <p>
* If the key is already associated with a value using the {@link #data(String, Object)} method then the mapping function is
* never used.
*
* @param key
* @param function
* @return self
*/
default TemplateInstance computedData(String key, Function<String, Object> function) {
throw new UnsupportedOperationException();
}

/**
*
* @param key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;

import org.jboss.logging.Logger;

Expand All @@ -16,7 +21,7 @@ public abstract class TemplateInstanceBase implements TemplateInstance {
static final Map<String, Object> EMPTY_DATA_MAP = Collections.singletonMap(DATA_MAP_KEY, true);

protected Object data;
protected Map<String, Object> dataMap;
protected DataMap dataMap;
protected final Map<String, Object> attributes;
protected List<Runnable> renderedActions;

Expand All @@ -35,10 +40,19 @@ public TemplateInstance data(Object data) {
public TemplateInstance data(String key, Object data) {
this.data = null;
if (dataMap == null) {
dataMap = new HashMap<String, Object>();
dataMap.put(DATA_MAP_KEY, true);
dataMap = new DataMap();
}
dataMap.put(key, data);
dataMap.put(Objects.requireNonNull(key), data);
return this;
}

@Override
public TemplateInstance computedData(String key, Function<String, Object> function) {
this.data = null;
if (dataMap == null) {
dataMap = new DataMap();
}
dataMap.computed(Objects.requireNonNull(key), Objects.requireNonNull(function));
return this;
}

Expand Down Expand Up @@ -86,11 +100,71 @@ protected Object data() {
return data;
}
if (dataMap != null) {
return Mapper.wrap(dataMap);
return dataMap;
}
return EMPTY_DATA_MAP;
}

protected abstract Engine engine();

public static class DataMap implements Mapper {

private final Map<String, Object> map = new HashMap<>();
private Map<String, Function<String, Object>> computations = null;

void put(String key, Object value) {
map.put(key, value);
}

void computed(String key, Function<String, Object> function) {
if (!map.containsKey(key)) {
if (computations == null) {
computations = new HashMap<>();
}
computations.put(key, function);
}
}

@Override
public Object get(String key) {
Object val = map.get(key);
if (val == null) {
if (key.equals(DATA_MAP_KEY)) {
return true;
} else if (computations != null) {
Function<String, Object> fun = computations.get(key);
if (fun != null) {
return fun.apply(key);
}
}
}
return val;
}

@Override
public boolean appliesTo(String key) {
return map.containsKey(key) || (computations != null && computations.containsKey(key));
}

@Override
public Set<String> mappedKeys() {
Set<String> ret = new HashSet<>(map.keySet());
if (computations != null) {
ret.addAll(computations.keySet());
}
return ret;
}

public void forEachData(BiConsumer<String, Object> action) {
map.forEach(action);
}

public void forEachComputedData(BiConsumer<String, Function<String, Object>> action) {
if (computations != null) {
computations.forEach(action);
}
}

}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.qute;

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

import java.util.concurrent.atomic.AtomicBoolean;
Expand Down Expand Up @@ -41,4 +42,26 @@ public void testGetTemplate() {
String generatedId = hello.getGeneratedId();
assertEquals(generatedId, hello.instance().getTemplate().getGeneratedId());
}

@Test
public void testComputeData() {
Engine engine = Engine.builder().addDefaults().build();
TemplateInstance instance = engine.parse("Hello {foo} and {bar}!").instance();
AtomicBoolean barUsed = new AtomicBoolean();
AtomicBoolean fooUsed = new AtomicBoolean();
instance
.computedData("bar", key -> {
barUsed.set(true);
return key.length();
})
.data("bar", 30)
.computedData("foo", key -> {
fooUsed.set(true);
return key.toUpperCase();
});
assertFalse(fooUsed.get());
assertEquals("Hello FOO and 30!", instance.render());
assertTrue(fooUsed.get());
assertFalse(barUsed.get());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -95,6 +96,9 @@ private Descriptors() {
NotFound.class, EvalContext.class);
public static final MethodDescriptor TEMPLATE_INSTANCE_DATA = MethodDescriptor.ofMethod(TemplateInstance.class, "data",
TemplateInstance.class, String.class, Object.class);
public static final MethodDescriptor TEMPLATE_INSTANCE_COMPUTED_DATA = MethodDescriptor.ofMethod(TemplateInstance.class,
"computedData",
TemplateInstance.class, String.class, Function.class);

public static final FieldDescriptor EVALUATED_PARAMS_STAGE = FieldDescriptor.of(EvaluatedParams.class, "stage",
CompletionStage.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.function.Function;

import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
Expand All @@ -24,6 +25,7 @@
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.ClassOutput;
import io.quarkus.gizmo.FieldDescriptor;
import io.quarkus.gizmo.FunctionCreator;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
Expand Down Expand Up @@ -74,22 +76,27 @@ public void generate(ClassInfo declaringClass, Map<String, AnnotationTarget> tar

for (Entry<String, AnnotationTarget> entry : targets.entrySet()) {
ResultHandle name = accept.load(entry.getKey());
FunctionCreator fun = accept.createFunction(Function.class);
BytecodeCreator funBytecode = fun.getBytecode();
ResultHandle global;
switch (entry.getValue().kind()) {
case FIELD:
FieldInfo field = entry.getValue().asField();
validate(field);
global = accept.readStaticField(FieldDescriptor.of(field));
global = funBytecode.readStaticField(FieldDescriptor.of(field));
break;
case METHOD:
MethodInfo method = entry.getValue().asMethod();
validate(method);
global = accept.invokeStaticMethod(MethodDescriptor.of(method));
global = funBytecode.invokeStaticMethod(MethodDescriptor.of(method));
break;
default:
throw new IllegalStateException("Unsupported target: " + entry.getValue());
}
accept.invokeInterfaceMethod(Descriptors.TEMPLATE_INSTANCE_DATA, accept.getMethodParam(0), name, global);
funBytecode.returnValue(global);
// Global variables are computed lazily
accept.invokeInterfaceMethod(Descriptors.TEMPLATE_INSTANCE_COMPUTED_DATA, accept.getMethodParam(0), name,
fun.getInstance());
}
accept.returnValue(null);

Expand Down

0 comments on commit 510e89e

Please sign in to comment.