Skip to content

Commit 74d7d69

Browse files
authored
Merge pull request quarkusio#47526 from mkouba/issue-34315
Qute: introduce equals operator for output expressions
2 parents 3830bb5 + c0fe20d commit 74d7d69

File tree

8 files changed

+144
-14
lines changed

8 files changed

+144
-14
lines changed

docs/src/main/asciidoc/qute-reference.adoc

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -399,28 +399,32 @@ NOTE: The current context can be accessed via the implicit binding `this`.
399399
|===
400400
|Name |Description |Examples
401401

402-
|Elvis Operator
402+
|Elvis Operator: `?:`
403403
|Outputs the default value if the previous part cannot be resolved or resolves to `null`.
404404
|`{person.name ?: 'John'}`, `{person.name or 'John'}`, `{person.name.or('John')}`
405405

406-
|orEmpty
406+
|`orEmpty`
407407
|Outputs an empty list if the previous part cannot be resolved or resolves to `null`.
408408
|`{pets.orEmpty.size}` outputs `0` if `pets` is not resolvable or `null`
409409

410-
|Ternary Operator
410+
|Ternary Operator: `condition ? ifTrue : ifFalse`
411411
|Shorthand for if-then-else statement. Unlike in <<if_section>> nested operators are not supported.
412412
|`{item.isActive ? item.name : 'Inactive item'}` outputs the value of `item.name` if `item.isActive` resolves to `true`.
413413

414-
|Logical AND Operator
414+
|Logical AND Operator: `&&`
415415
|Outputs `true` if both parts are not `falsy` as described in the <<if_section>>.
416416
The parameter is only evaluated if needed.
417417
|`{person.isActive && person.hasStyle}`
418418

419-
|Logical OR Operator
419+
|Logical OR Operator: `\|\|`
420420
|Outputs `true` if any of the parts is not `falsy` as described in the <<if_section>>.
421421
The parameter is only evaluated if needed.
422422
|`{person.isActive \|\| person.hasStyle}`
423423

424+
|Equals Operator: `==`/`eq`/`is`
425+
|Outputs `true` if the base object is equal to the argument.
426+
|`{obj1 is obj2 ? 'Equal' : 'Inequal'}`, `{obj1 == obj2 ? 'Equal' : 'Inequal'}`, `{obj1.eq(obj2) ? 'Equal' : 'Inequal'}`
427+
424428
|===
425429

426430
TIP: The condition in a ternary operator evaluates to `true` if the value is not considered `falsy` as described in <<if_section>>.
@@ -2263,22 +2267,23 @@ NOTE: Superfluous matching conditions are ignored. The conditions sorted by prio
22632267

22642268
An extension method may declare parameters.
22652269
If no namespace is specified then the first parameter that is not annotated with `@TemplateAttribute` is used to pass the base object, i.e. `org.acme.Item` in the first example.
2266-
If matching any name or using a regular expression, then a string method parameter needs to be used to pass the property name.
2270+
If matching any name or using a regular expression, then a string method parameter (not not annotated with `@TemplateAttribute`) needs to be used to pass the property name.
22672271
Parameters annotated with `@TemplateAttribute` are obtained via `TemplateInstance#getAttribute()`.
2268-
All other parameters are resolved when rendering the template and passed to the extension method.
2272+
All other parameters are treated as virtual method parameters and resolved when rendering the template and passed to the extension method.
22692273

22702274
.Multiple Parameters Example
22712275
[source,java]
22722276
----
22732277
@TemplateExtension
22742278
class BigDecimalExtensions {
22752279
2276-
static BigDecimal scale(BigDecimal val, int scale, RoundingMode mode) { <1>
2280+
@TemplateExtension(matchNames = {"scale", "setScale"})
2281+
static BigDecimal scale(BigDecimal val, String ignoredName, int scale, RoundingMode mode) { <1>
22772282
return val.setScale(scale, mode);
22782283
}
22792284
}
22802285
----
2281-
<1> This method matches an expression with base object of the type `BigDecimal.class`, with the `scale` virtual method name and two virtual method parameters.
2286+
<1> This method matches an expression with base object of the type `BigDecimal.class`, with the `scale()`/`setScale()` virtual method name and two virtual method parameters - `scale` and `mode`.
22822287

22832288
[source,html]
22842289
----

extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@
155155
import io.quarkus.qute.runtime.extensions.ConfigTemplateExtensions;
156156
import io.quarkus.qute.runtime.extensions.MapTemplateExtensions;
157157
import io.quarkus.qute.runtime.extensions.NumberTemplateExtensions;
158+
import io.quarkus.qute.runtime.extensions.ObjectsTemplateExtensions;
158159
import io.quarkus.qute.runtime.extensions.OrOperatorTemplateExtensions;
159160
import io.quarkus.qute.runtime.extensions.StringTemplateExtensions;
160161
import io.quarkus.qute.runtime.extensions.TimeTemplateExtensions;
@@ -285,7 +286,8 @@ AdditionalBeanBuildItem additionalBeans() {
285286
.addBeanClasses(EngineProducer.class, TemplateProducer.class, ContentTypes.class, Template.class,
286287
TemplateInstance.class, CollectionTemplateExtensions.class,
287288
MapTemplateExtensions.class, NumberTemplateExtensions.class, ConfigTemplateExtensions.class,
288-
TimeTemplateExtensions.class, StringTemplateExtensions.class, OrOperatorTemplateExtensions.class)
289+
TimeTemplateExtensions.class, StringTemplateExtensions.class, OrOperatorTemplateExtensions.class,
290+
ObjectsTemplateExtensions.class)
289291
.build();
290292
}
291293

@@ -422,7 +424,7 @@ && isNotLocatedByCustomTemplateLocator(locatorPatternsBuildItem.getLocationPatte
422424
}
423425
reservedNames.add(method.name());
424426
}
425-
for (ClassInfo recordClass : index.getIndex().getAllKnownImplementors(recordInterfaceName)) {
427+
for (ClassInfo recordClass : index.getIndex().getAllKnownImplementations(recordInterfaceName)) {
426428
if (!recordClass.isRecord()) {
427429
continue;
428430
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.quarkus.qute.deployment.extensions;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.util.function.IntFunction;
6+
7+
import jakarta.inject.Inject;
8+
9+
import org.jboss.shrinkwrap.api.asset.StringAsset;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.RegisterExtension;
12+
13+
import io.quarkus.qute.Template;
14+
import io.quarkus.test.QuarkusUnitTest;
15+
16+
public class ObjectsTemplateExtensionsTest {
17+
18+
@RegisterExtension
19+
static final QuarkusUnitTest config = new QuarkusUnitTest()
20+
.withApplicationRoot(root -> root
21+
.addAsResource(new StringAsset(
22+
"{name == 'hallo' ? 'yes' : 'no'}::{name eq 'hello' ? 'yes' : 'no'}::{name is 'hi' ? 'yes' : 'no'}::{#if name eq 'hallo'}ok{/if}"
23+
+ "::{fun.apply(name eq 'hello' ? 0 : 7)}"),
24+
"templates/foo.html"));
25+
26+
@Inject
27+
Template foo;
28+
29+
@Test
30+
public void testEquals() {
31+
assertEquals("yes::no::no::ok::ok", foo
32+
.data("name", "hallo")
33+
.data("fun", new IntFunction<String>() {
34+
35+
@Override
36+
public String apply(int value) {
37+
return value > 5 ? "ok" : "nok";
38+
}
39+
})
40+
.render());
41+
}
42+
43+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.quarkus.qute.runtime.extensions;
2+
3+
import java.util.Objects;
4+
5+
import jakarta.enterprise.inject.Vetoed;
6+
7+
import io.quarkus.qute.TemplateExtension;
8+
9+
@Vetoed // Make sure no bean is created from this class
10+
public class ObjectsTemplateExtensions {
11+
12+
@TemplateExtension(matchNames = { "eq", "==", "is" })
13+
static boolean eq(Object value, String ignoredName, Object other) {
14+
return Objects.equals(value, other);
15+
}
16+
}

independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public EngineBuilder addDefaultValueResolvers() {
156156
ValueResolvers.thisResolver(), ValueResolvers.orResolver(), ValueResolvers.trueResolver(),
157157
ValueResolvers.logicalAndResolver(), ValueResolvers.logicalOrResolver(), ValueResolvers.orEmpty(),
158158
ValueResolvers.arrayResolver(), ValueResolvers.plusResolver(), ValueResolvers.minusResolver(),
159-
ValueResolvers.modResolver(), ValueResolvers.numberValueResolver());
159+
ValueResolvers.modResolver(), ValueResolvers.numberValueResolver(), ValueResolvers.equalsResolver());
160160
}
161161

162162
/**

independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateExtension.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@
4040
* <h2>Matching by name</h2>
4141
*
4242
* By default, the method name is used to match the expression property/method name. However, it is possible to specify the
43-
* matching name with
44-
* {@link #matchName()}.
43+
* matching name with {@link #matchName()}.
4544
*
4645
* <pre>
4746
* {@literal @}TemplateExtension(matchName = "discounted")
@@ -107,18 +106,21 @@
107106
int DEFAULT_PRIORITY = 5;
108107

109108
/**
109+
* If {@link #ANY} is used then an additional string method parameter must be used to pass the actual property name.
110110
*
111111
* @return the name is used to match the property name
112112
*/
113113
String matchName() default METHOD_NAME;
114114

115115
/**
116+
* Note that an additional string method parameter must be used to pass the actual property name.
116117
*
117118
* @return the list of names used to match the property name
118119
*/
119120
String[] matchNames() default {};
120121

121122
/**
123+
* Note that an additional string method parameter must be used to pass the actual property name.
122124
*
123125
* @return the regex is used to match the property name
124126
*/

independent-projects/qute/core/src/main/java/io/quarkus/qute/ValueResolvers.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.Map;
1010
import java.util.Map.Entry;
1111
import java.util.NoSuchElementException;
12+
import java.util.Objects;
1213
import java.util.Optional;
1314
import java.util.concurrent.CompletionStage;
1415
import java.util.function.Function;
@@ -525,6 +526,34 @@ protected Object compute(Integer op1, Integer op2) {
525526

526527
}
527528

529+
public static ValueResolver equalsResolver() {
530+
return new EqualsResolver();
531+
}
532+
533+
public static final class EqualsResolver implements ValueResolver {
534+
535+
public boolean appliesTo(EvalContext context) {
536+
if (context.getParams().size() != 1) {
537+
return false;
538+
}
539+
String name = context.getName();
540+
return name.equals("==") || name.equals("eq") || name.equals("is");
541+
}
542+
543+
@Override
544+
public CompletionStage<Object> resolve(EvalContext context) {
545+
Object base = context.getBase();
546+
Expression otherExpr = context.getParams().get(0);
547+
if (otherExpr.isLiteral()) {
548+
Object literalValue = otherExpr.getLiteral();
549+
return CompletedStage.of(Objects.equals(base, literalValue));
550+
} else {
551+
return context.evaluate(otherExpr).thenApply(other -> Objects.equals(base, other));
552+
}
553+
}
554+
555+
}
556+
528557
static abstract class IntArithmeticResolver implements ValueResolver {
529558

530559
public boolean appliesTo(EvalContext context) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.quarkus.qute;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.util.function.Function;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
public class EqualsResolverTest {
10+
11+
@Test
12+
public void tesEqualsResolver() {
13+
Engine engine = Engine.builder().addDefaults().addValueResolver(new ReflectionValueResolver()).build();
14+
assertEquals("false",
15+
engine.parse("{name eq 'Andy'}").data("name", "Martin").render());
16+
assertEquals("true",
17+
engine.parse("{name is 'Andy'}").data("name", "Andy").render());
18+
assertEquals("true",
19+
engine.parse("{name == 'Andy'}").data("name", "Andy").render());
20+
assertEquals("true",
21+
engine.parse("{negate.apply(name is 'Andy')}").data("name", "David").data("negate", new NegateFun()).render());
22+
}
23+
24+
public static class NegateFun implements Function<Boolean, Boolean> {
25+
26+
@Override
27+
public Boolean apply(Boolean value) {
28+
return !value;
29+
}
30+
31+
}
32+
33+
}

0 commit comments

Comments
 (0)