Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Qute - prefix iteration metadata in a loop #21000

Merged
merged 1 commit into from
Nov 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -557,26 +557,55 @@ The other form is using the `for` name and can specify the alias used to referen
----
<1> `item` is the alias used for the iteration element.

It's also possible to access the iteration metadata inside the loop:
It's also possible to access the iteration metadata inside the loop via the following keys:

* `count` - 1-based index
* `index` - zero-based index
* `hasNext` - `true` if the iteration has more elements
* `isLast` - `true` if `hasNext == false`
* `isFirst` - `true` if `count == 1`
* `odd` - `true` if the zero-based index is odd
* `even` - `true` if the zero-based index is even
* `indexParity` - outputs `odd` or `even` based on the zero-based index value

However, the keys cannot be used directly.
Instead, a prefix is used to avoid possible collisions with variables from the outer scope.
By default, the alias of an iterated element suffixed with an underscore is used as a prefix.
For example, the `hasNext` key must be prefixed with `it_` inside an `{#each}` section: `{it_hasNext}`.

.`each` Iteration Metadata Example
[source]
----
{#each items}
{it_count}. {it.name} <1>
{#if it_hasNext}<br>{/if} <2>
{/each}
----
<1> `it_count` represents one-based index.
<2> `<br>` is only rendered if the iteration has more elements.

* `{count}` - 1-based index
* `{index}` - zero-based index
* `{hasNext}` - `true` if the iteration has more elements
* `{odd}` - `true` if the zero-based index is odd
* `{even}` - `true` if the zero-based index is even
* `{indexParity}` - outputs `odd` or `even` based on the zero-based index value
And must be used in a form of `{item_hasNext}` inside a `{#for}` section with the `item` element alias.

.Iteration Metadata Example
.`for` Iteration Metadata Example
[source]
----
{#each items}
{count}. {it.name} <1>
{#if hasNext}<br>{/if} <2>
{#for item in items}
{item_count}. {item.name} <1>
{#if item_hasNext}<br>{/if} <2>
{/each}
----
<1> `count` represents one-based index.
<1> `item_count` represents one-based index.
<2> `<br>` is only rendered if the iteration has more elements.

[TIP]
====
The iteration metadata prefix is configurable either via `EngineBuilder.iterationMetadataPrefix()` for standalone Qute or via the `quarkus.qute.iteration-metadata-prefix` configuration property in a Quarkus application. Three special constants can be used:

1. `<alias_>` - the alias of an iterated element suffixed with an underscore is used (default)
2. `<alias?>` - the alias of an iterated element suffixed with a question mark is used
3. `<none>` - no prefix is used
====

The `for` statement also works with integers, starting from 1. In the example below, considering that `total = 3`:

[source]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<tbody>
{#for bean in info:devBeanInfos.beans}
<tr>
<td>{count}.</td>
<td>{bean_count}.</td>
<td>
{#display-bean bean/}
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
<tbody>
{#for interceptor in info:devBeanInfos.interceptors}
<tr>
<td>{count}.</td>
<td>{interceptor_count}.</td>
<td><span class="class-candidate">{interceptor.interceptorClass}</span></td>
<td><span class="badge rounded-pill bg-info text-light larger-badge" title="Priority: {interceptor.priority}">{interceptor.priority}</span></td>
<td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<tbody>
{#for observer in info:devBeanInfos.observers}
<tr>
<td>{count}.</td>
<td>{observer_count}.</td>
<td>
{#if observer.declaringClass}
<span class="class-candidate">{observer.declaringClass}</span>#{observer.methodName}()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<tbody>
{#for bean in info:devBeanInfos.removedBeans}
<tr>
<td>{count}.</td>
<td>{bean_count}.</td>
<td>
{#display-bean bean/}
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<tbody>
{#for service in info:grpcServices.infos}
<tr>
<td>{count}.</td>
<td>{service_count}.</td>
<td>
{#when service.status}
{#is SERVING}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ <h4><span class="badge">Persistence Unit</span> <i class="badge badge-info">{pu.
<tbody>
{#for entity in pu.managedEntities}
<tr>
<td>{count}.</td>
<td>{entity_count}.</td>
<td>{entity.className}</td>
<td>{entity.tableName}</td>
</tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ <h4><span class="badge">Persistence Unit</span> <i class="badge badge-info">{pu.
<tbody>
{#for query in pu.allNamedQueries}
<tr>
<td>{count}.</td>
<td>{query_count}.</td>
<td>{query.name}</td>
<td>{query.query}</td>
<td>{query.lockMode}</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,37 +32,37 @@
<thead class="thead-light">
<tr>
<th scope="col">
<a href="#" onclick="toggleExpanded(this, 'td-create-script-{count}'); return false;" class="script-heading">
<a href="#" onclick="toggleExpanded(this, 'td-create-script-{pu_count}'); return false;" class="script-heading">
<span class="fa fa-chevron-right icon"></span>
<span>Create Script</span>
</a>
<a href="#" onclick="copyToClipboard('create-script-{count}'); return false;" class="float-right badge">
<a href="#" onclick="copyToClipboard('create-script-{pu_count}'); return false;" class="float-right badge">
<span class="fa fa-clipboard"></span> Copy</a>
</th>
</tr>
</thead>
<tbody>
<tr>
<td id="td-create-script-{count}" class="hidden">
<pre id="create-script-{count}" class="ddl-script">{pu.createDDL}</pre>
<td id="td-create-script-{pu_count}" class="hidden">
<pre id="create-script-{pu_count}" class="ddl-script">{pu.createDDL}</pre>
</td>
</tr>
<thead class="thead-light">
<tr>
<th scope="col">
<a href="#" onclick="toggleExpanded(this, 'td-drop-script-{count}'); return false;" class="script-heading">
<a href="#" onclick="toggleExpanded(this, 'td-drop-script-{pu_count}'); return false;" class="script-heading">
<span class="fa fa-chevron-right icon"></span>
<span>Drop Script</span>
</a>
<a href="#" onclick="copyToClipboard('drop-script-{count}'); return false;" class="float-right badge">
<a href="#" onclick="copyToClipboard('drop-script-{pu_count}'); return false;" class="float-right badge">
<span class="fa fa-clipboard"></span> Copy</a>
</th>
</tr>
</thead>
<tbody>
<tr>
<td id="td-drop-script-{count}" class="hidden">
<pre id="drop-script-{count}" class="ddl-script">{pu.dropDDL}</pre>
<td id="td-drop-script-{pu_count}" class="hidden">
<pre id="drop-script-{pu_count}" class="ddl-script">{pu.dropDDL}</pre>
</td>
</tr>
</tbody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ public String apply(String id) {
for (Expression param : params) {
if (param.hasTypeInfo()) {
Map<String, Match> results = new HashMap<>();
QuteProcessor.validateNestedExpressions(exprEntry.getKey(), defaultBundleInterface,
QuteProcessor.validateNestedExpressions(config, exprEntry.getKey(), defaultBundleInterface,
results, excludes, incorrectExpressions, expression, index,
implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches,
checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ public class QuteProcessor {
private static final String CHECKED_TEMPLATE_BASE_PATH = "basePath";
private static final String BASE_PATH = "templates";

private static final Set<String> ITERATION_METADATA_KEYS = Set.of("count", "index", "indexParity", "hasNext", "odd",
"isOdd", "even", "isEven", "isLast", "isFirst");

private static final Function<FieldInfo, String> GETTER_FUN = new Function<FieldInfo, String>() {
@Override
public String apply(FieldInfo field) {
Expand Down Expand Up @@ -527,7 +530,8 @@ public String apply(String id) {
if (expression.isLiteral()) {
continue;
}
Match match = validateNestedExpressions(templateAnalysis, null, new HashMap<>(), excludes, incorrectExpressions,
Match match = validateNestedExpressions(config, templateAnalysis, null, new HashMap<>(), excludes,
incorrectExpressions,
expression, index, implicitClassToMembersUsed, templateIdToPathFun, generatedIdsToMatches,
checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData, regularExtensionMethods,
namespaceExtensionMethods);
Expand Down Expand Up @@ -587,7 +591,8 @@ static String buildIgnorePattern(Iterable<String> names) {
return ignorePattern.toString();
}

static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassInfo rootClazz, Map<String, Match> results,
static Match validateNestedExpressions(QuteConfig config, TemplateAnalysis templateAnalysis, ClassInfo rootClazz,
Map<String, Match> results,
List<TypeCheckExcludeBuildItem> excludes, BuildProducer<IncorrectExpressionBuildItem> incorrectExpressions,
Expression expression, IndexView index,
Map<DotName, Set<String>> implicitClassToMembersUsed, Function<String, String> templateIdToPathFun,
Expand All @@ -606,7 +611,7 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI
continue;
}
if (!results.containsKey(param.toOriginalString())) {
validateNestedExpressions(templateAnalysis, null, results, excludes,
validateNestedExpressions(config, templateAnalysis, null, results, excludes,
incorrectExpressions, param, index, implicitClassToMembersUsed, templateIdToPathFun,
generatedIdsToMatches, checkedTemplate, lookupConfig, namedBeans, namespaceTemplateData,
regularExtensionMethods, namespaceExtensionMethods);
Expand Down Expand Up @@ -651,14 +656,39 @@ static Match validateNestedExpressions(TemplateAnalysis templateAnalysis, ClassI
}

if (checkedTemplate != null && checkedTemplate.requireTypeSafeExpressions && !expression.hasTypeInfo()) {
incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
"Only type-safe expressions are allowed in the checked template defined via: "
+ checkedTemplate.method.declaringClass().name() + "."
+ checkedTemplate.method.name()
+ "(); an expression must be based on a checked template parameter "
+ checkedTemplate.bindings.keySet()
+ ", or bound via a param declaration, or the requirement must be relaxed via @CheckedTemplate(requireTypeSafeExpressions = false)",
expression.getOrigin()));
if (!expression.hasNamespace() && expression.getParts().size() == 1
&& ITERATION_METADATA_KEYS.contains(expression.getParts().get(0).getName())) {
String prefixInfo;
if (config.iterationMetadataPrefix
.equals(LoopSectionHelper.Factory.ITERATION_METADATA_PREFIX_ALIAS_UNDERSCORE)) {
prefixInfo = String.format(
"based on the iteration alias, i.e. the correct key should be something like {it_%1$s} or {element_%1$s}",
expression.getParts().get(0).getName());
} else if (config.iterationMetadataPrefix
.equals(LoopSectionHelper.Factory.ITERATION_METADATA_PREFIX_ALIAS_QM)) {
prefixInfo = String.format(
"based on the iteration alias, i.e. the correct key should be something like {it?%1$s} or {element?%1$s}",
expression.getParts().get(0).getName());
} else {
prefixInfo = ": " + config.iterationMetadataPrefix + ", i.e. the correct key should be: "
+ config.iterationMetadataPrefix + expression.getParts().get(0).getName();
}
incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
"An invalid iteration metadata key is probably used\n\t- The configured iteration metadata prefix is "
+ prefixInfo
+ "\n\t- You can configure the prefix via the io.quarkus.qute.iteration-metadata-prefix configuration property",
expression.getOrigin()));
} else {
incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
"Only type-safe expressions are allowed in the checked template defined via: "
+ checkedTemplate.method.declaringClass().name() + "."
+ checkedTemplate.method.name()
+ "(); an expression must be based on a checked template parameter "
+ checkedTemplate.bindings.keySet()
+ ", or bound via a param declaration, or the requirement must be relaxed via @CheckedTemplate(requireTypeSafeExpressions = false)",
expression.getOrigin()));

}
return putResult(match, results, expression);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
{#if template.parameters}
testData{dataCount} = '{';
{#each template.parameters}
testData{dataCount} += '\n // Template parameter {count}: {it.value.raw}\n';
testData{dataCount} += '\n // Template parameter {template_count}: {it.value.raw}\n';
testData{dataCount} += ' "{it.key}" : null';
{#if hasNext}
testData{dataCount} += ',';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<tbody>
{#for template in info:devQuteInfos.templates}
<tr>
<td>{count}.</td>
<td>{template_count}.</td>
<td>
{template.path}
</td>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.quarkus.qute.deployment.loop;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class IterationMetadataPrefixAliasQuestionMarkTest extends IterationMetadataPrefixTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource(new StringAsset("{#for i in total}{i?count}:{i}{#if i?hasNext}::{/if}{/for}"),
"templates/loop.html"))
.overrideConfigKey("quarkus.qute.iteration-metadata-prefix", "<alias?>");

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.quarkus.qute.deployment.loop;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class IterationMetadataPrefixNoneTest extends IterationMetadataPrefixTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addAsResource(new StringAsset("{#for i in total}{count}:{i}{#if hasNext}::{/if}{/for}"),
"templates/loop.html"))
.overrideConfigKey("quarkus.qute.iteration-metadata-prefix", "<none>");

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.qute.deployment.loop;

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

import javax.inject.Inject;

import org.junit.jupiter.api.Test;

import io.quarkus.qute.Template;

public abstract class IterationMetadataPrefixTest {

@Inject
Template loop;

@Test
public void testPrefix() {
assertEquals("1:1::2:2::3:3", loop.data("total", 3).render());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ public class CheckedTemplateRequireTypeSafeTest {
+ "{inject:fool.getJoke(null)} "
+ "{inject:fool.getJoke(identifier)} "
+ "{#each name.chars.iterator}"
+ "{! {index} is not considered an error because the binding is registered by the loop section !}"
+ "{index}. {it}"
// {it_index} is not considered an error because the binding is registered by the loop section !}
+ "{it_index}."
// however, {index} is an error
+ "{index}"
+ "{it}"
+ "{/each}"),
"templates/CheckedTemplateRequireTypeSafeTest/hola.txt"))
.assertException(t -> {
Expand All @@ -45,9 +48,10 @@ public class CheckedTemplateRequireTypeSafeTest {
e = e.getCause();
}
assertNotNull(te);
assertTrue(te.getMessage().contains("Found template problems (2)"), te.getMessage());
assertTrue(te.getMessage().contains("Found template problems (3)"), te.getMessage());
assertTrue(te.getMessage().contains("any"), te.getMessage());
assertTrue(te.getMessage().contains("identifier"), te.getMessage());
assertTrue(te.getMessage().contains("index"), te.getMessage());
});

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class ParamDeclarationTest {
.addAsResource(new StringAsset("{@io.quarkus.qute.deployment.typesafe.Movie movie}"
+ "{movie.mainCharacters.size}: {#for character in movie.mainCharacters}"
+ "{character}"
+ "{#if hasNext}, {/}"
+ "{#if character_hasNext}, {/}"
+ "{/}"), "templates/movie.html"));

@Inject
Expand Down
Loading