Init (1)
All checks were successful
Maven build / build (push) Successful in 1m39s

This commit is contained in:
Guillaume Dugas
2026-02-19 09:58:35 +01:00
parent 7ad05e5b14
commit b205c0d18a
46 changed files with 2397 additions and 66 deletions

View File

@@ -0,0 +1,60 @@
package fr.cnd.compositor.blocks;
import fr.cnd.compositor.models.BlockRenderRequest;
import fr.cnd.compositor.models.BlockRenderResult;
import fr.cnd.compositor.blocks.services.BlockService;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Path("/blocks")
public class BlockResource {
@Inject
BlockService blockService;
@POST
@Path("template/render")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Uni<BlockRenderResult> render(BlockRenderRequest request) {
final Map<String, List<String>> result = new HashMap<>();
Uni<Void> chain = Uni.createFrom().voidItem();
for (final String definitionId : request.getDefinitions().keySet()) {
final List<String> definitionResults = new ArrayList<>();
result.put(definitionId, definitionResults);
chain = chain.chain(() -> blockService.renderTemplate(request.getDefinitions().get(definitionId))
.invoke(definitionResults::add))
.replaceWithVoid();
}
return chain.map(x -> BlockRenderResult.builder().result(result).build());
}
@POST
@Path("configuration/render")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Uni<BlockRenderResult> renderConfiguration(BlockRenderRequest request) {
final Map<String, List<String>> result = new HashMap<>();
Uni<Void> chain = Uni.createFrom().voidItem();
for (final String definitionId : request.getDefinitions().keySet()) {
final List<String> definitionResults = new ArrayList<>();
result.put(definitionId, definitionResults);
chain = chain.chain(() -> blockService.renderBlockConfiguration(request.getDefinitions().get(definitionId))
.invoke(definitionResults::add))
.replaceWithVoid();
}
return chain.map(x -> BlockRenderResult.builder().result(result).build());
}
}

View File

@@ -1,17 +0,0 @@
package fr.cnd.compositor.blocks.models;
import lombok.*;
import java.util.List;
import java.util.Map;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BlockRenderDefinition {
String name;
Map<String, Object> inputs;
Map<String, List<BlockRenderDefinition>> slots;
}

View File

@@ -1,15 +0,0 @@
package fr.cnd.compositor.blocks.models;
import lombok.*;
import java.util.List;
import java.util.Map;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BlockRenderRequest {
Map<String, List<BlockRenderDefinition>> definitions;
}

View File

@@ -1,15 +0,0 @@
package fr.cnd.compositor.blocks.models;
import lombok.*;
import java.util.List;
import java.util.Map;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BlockRenderResult {
Map<String, List<String>> result;
}

View File

@@ -1,6 +1,6 @@
package fr.cnd.compositor.blocks.pebble;
import fr.cnd.compositor.blocks.models.BlockRenderDefinition;
import fr.cnd.compositor.models.BlockRenderDefinition;
import fr.cnd.compositor.blocks.specs.BlockTemplate;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;

View File

@@ -0,0 +1,33 @@
package fr.cnd.compositor.blocks.pebble;
import fr.cnd.compositor.blocks.specs.BlockEngine;
import io.pebbletemplates.pebble.PebbleEngine;
import io.pebbletemplates.pebble.template.PebbleTemplate;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.io.StringWriter;
import java.util.Map;
@ApplicationScoped
public class PebbleBlockEngine implements BlockEngine {
@Inject
PebbleEngine pebbleEngine;
@Override
public Uni<String> render(String content, Map<String, Object> context) {
return Uni.createFrom().item(Unchecked.supplier(() -> {
try {
PebbleTemplate template = pebbleEngine.getTemplate(content);
StringWriter writer = new StringWriter();
template.evaluate(writer, context);
return writer.toString();
} catch (Exception e) {
throw new RuntimeException("Failed to render template content", e);
}
}));
}
}

View File

@@ -0,0 +1,19 @@
package fr.cnd.compositor.blocks.pebble;
import io.pebbletemplates.pebble.PebbleEngine;
import io.pebbletemplates.pebble.loader.StringLoader;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
public class PebbleProvider {
@Inject
PebbleBlocksExtension pebbleBlocksExtension;
@ApplicationScoped
PebbleEngine createPebbleEngine() {
return new PebbleEngine.Builder()
.loader(new StringLoader())
.extension(pebbleBlocksExtension)
.build();
}
}

View File

@@ -0,0 +1,27 @@
package fr.cnd.compositor.blocks.resolvers;
import fr.cnd.compositor.blocks.specs.BlockConfiguration;
import fr.cnd.compositor.blocks.specs.BlockConfigurationResolver;
import io.smallrye.common.annotation.Identifier;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
@ApplicationScoped
public class CDIBlockConfigurationResolver implements BlockConfigurationResolver {
@Inject
@Any
Instance<BlockConfiguration> blockConfigurations;
@Override
public Uni<BlockConfiguration> resolveConfiguration(String blockIdentifier) {
Instance<BlockConfiguration> selected = blockConfigurations.select(Identifier.Literal.of(blockIdentifier));
if (selected.isResolvable()) {
return Uni.createFrom().item(selected.get());
}
return Uni.createFrom().nullItem();
}
}

View File

@@ -0,0 +1,27 @@
package fr.cnd.compositor.blocks.resolvers;
import fr.cnd.compositor.blocks.specs.BlockTemplate;
import fr.cnd.compositor.blocks.specs.BlockTemplateResolver;
import io.smallrye.common.annotation.Identifier;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
@ApplicationScoped
public class CDIBlockTemplateResolver implements BlockTemplateResolver {
@Inject
@Any
Instance<BlockTemplate> blockTemplates;
@Override
public Uni<BlockTemplate> resolveTemplate(String blockIdentifier) {
Instance<BlockTemplate> selected = blockTemplates.select(Identifier.Literal.of(blockIdentifier));
if (selected.isResolvable()) {
return Uni.createFrom().item(selected.get());
}
return Uni.createFrom().nullItem();
}
}

View File

@@ -0,0 +1,27 @@
package fr.cnd.compositor.blocks.resolvers;
import fr.cnd.compositor.blocks.specs.ContextMapping;
import fr.cnd.compositor.blocks.specs.ContextMappingResolver;
import io.smallrye.common.annotation.Identifier;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
@ApplicationScoped
public class CDIContextMappingResolver implements ContextMappingResolver {
@Inject
@Any
Instance<ContextMapping> contextMappings;
@Override
public Uni<ContextMapping> resolveContextMapping(String contextMappingIdentifier) {
Instance<ContextMapping> selected = contextMappings.select(Identifier.Literal.of(contextMappingIdentifier));
if (selected.isResolvable()) {
return Uni.createFrom().item(selected.get());
}
return Uni.createFrom().nullItem();
}
}

View File

@@ -0,0 +1,161 @@
package fr.cnd.compositor.blocks.services;
import fr.cnd.compositor.models.BlockRenderDefinition;
import fr.cnd.compositor.blocks.specs.BlockConfiguration;
import fr.cnd.compositor.blocks.specs.BlockConfigurationResolver;
import fr.cnd.compositor.blocks.specs.BlockTemplate;
import fr.cnd.compositor.blocks.specs.BlockTemplateResolver;
import io.quarkus.arc.All;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.*;
/**
* Service responsible for resolving block templates by name.
* <p>
* This service aggregates all available {@link BlockTemplateResolver} implementations
* and queries them sequentially until one returns a non-null template. This allows
* multiple template sources (e.g., file-based, database-backed, remote) to coexist
* with a chain-of-responsibility pattern.
*/
@ApplicationScoped
public class BlockService {
@Inject
@All
List<BlockTemplateResolver> resolvers;
@Inject
@All
List<BlockConfigurationResolver> blockConfigurationResolvers;
/**
* Resolves a block template by its name.
* <p>
* Iterates through all registered {@link BlockTemplateResolver} instances and returns
* the first non-null {@link BlockTemplate} found. If no resolver can handle the template,
* returns {@code null}.
*
* @param templateName the identifier of the template to resolve
* @return a {@link Uni} emitting the resolved {@link BlockTemplate}, or {@code null} if not found
*/
public Uni<BlockTemplate> resolveTemplate(String templateName) {
Uni<BlockTemplate> result = Uni.createFrom().nullItem();
for (BlockTemplateResolver resolver : resolvers) {
result = result.chain(template -> {
if (template != null) {
return Uni.createFrom().item(template);
}
return resolver.resolveTemplate(templateName);
});
}
return result;
}
/**
* Resolves a block configuration by its identifier.
* <p>
* Iterates through all registered {@link BlockConfigurationResolver} instances and returns
* the first non-null {@link BlockConfiguration} found. If no resolver can handle the identifier,
* returns {@code null}.
*
* @param blockIdentifier the identifier of the block configuration to resolve
* @return a {@link Uni} emitting the resolved {@link BlockConfiguration}, or {@code null} if not found
*/
public Uni<BlockConfiguration> resolveBlockConfiguration(String blockIdentifier) {
Uni<BlockConfiguration> result = Uni.createFrom().nullItem();
for (BlockConfigurationResolver resolver : blockConfigurationResolvers) {
result = result.chain(configuration -> {
if (configuration != null) {
return Uni.createFrom().item(configuration);
}
return resolver.resolveConfiguration(blockIdentifier);
});
}
return result;
}
public Uni<String> renderTemplate(List<BlockRenderDefinition> definitions) {
final List<String> results = new ArrayList<>();
Uni<Void> chain = Uni.createFrom().voidItem();
for (BlockRenderDefinition definition : definitions) {
chain = chain.chain(() -> renderTemplate(definition))
.invoke(results::add)
.replaceWithVoid();
}
return chain.map(x -> String.join(" ", results));
}
public Uni<String> renderTemplate(BlockRenderDefinition definition) {
final Map<String, String> slotsContent = new HashMap<>();
Uni<Void> slotChain = Uni.createFrom().voidItem();
for (String slotName : Optional.ofNullable(definition.getSlots()).map(Map::keySet).orElse(new HashSet<>())) {
slotChain = slotChain.chain(() -> renderTemplate(definition.getSlots().get(slotName)))
.invoke(slotContent -> slotsContent.put(slotName, slotContent))
.replaceWithVoid();
}
return slotChain.chain(() -> resolveTemplate(definition.getName()))
.chain(blockTemplate -> {
final Map<String, Object> context = new HashMap<>(Optional.ofNullable(definition.getInputs()).orElse(new HashMap<>()));
context.put("__slots__", slotsContent);
return blockTemplate.render(BlockRenderDefinition.builder()
.name(definition.getName())
.inputs(context)
.build());
});
}
public Uni<String> renderBlockConfiguration(List<BlockRenderDefinition> definitions) {
return mapDefinition(definitions)
.chain(this::renderTemplate);
}
private Uni<List<BlockRenderDefinition>> mapDefinition(List<BlockRenderDefinition> definitions) {
final List<BlockRenderDefinition> results = new ArrayList<>();
Uni<Void> chain = Uni.createFrom().voidItem();
for (BlockRenderDefinition definition : definitions) {
chain = chain.chain(() -> mapDefinition(definition))
.invoke(results::add)
.replaceWithVoid();
}
return chain.map(x -> results);
}
private Uni<BlockRenderDefinition> mapDefinition(BlockRenderDefinition definition) {
final Map<String, List<BlockRenderDefinition>> slotMapping = new HashMap<>();
Uni<Void> slotChain = Uni.createFrom().voidItem();
for (String slotName : Optional.ofNullable(definition.getSlots()).map(Map::keySet).orElse(new HashSet<>())) {
final List<BlockRenderDefinition> slotDefinitions = new ArrayList<>();
slotMapping.put(slotName, slotDefinitions);
for (BlockRenderDefinition blockDefinition : definition.getSlots().get(slotName)) {
slotChain = slotChain.chain(() -> mapDefinition(blockDefinition))
.invoke(slotDefinitions::add)
.replaceWithVoid();
}
}
final BlockRenderDefinition mappedDefinition = BlockRenderDefinition.builder()
.slots(slotMapping)
.build();
return slotChain.chain(() -> resolveBlockConfiguration(definition.getName())
.call(blockConfiguration -> blockConfiguration
.getBlockTemplateName()
.invoke(mappedDefinition::setName))
.call(blockConfiguration -> blockConfiguration
.mapInputs(definition.getInputs())
.invoke(mappedDefinition::setInputs))
.map(x -> mappedDefinition));
}
}

View File

@@ -0,0 +1,7 @@
package fr.cnd.compositor.blocks.specs;
import io.smallrye.mutiny.Uni;
public interface BlockConfigurationResolver {
Uni<BlockConfiguration> resolveConfiguration(String blockIdentifier);
}

View File

@@ -0,0 +1,52 @@
package fr.cnd.compositor.blocks.specs;
import io.smallrye.mutiny.Uni;
import fr.cnd.compositor.blocks.pebble.PebbleBlockEngine;
import java.util.Map;
/**
* Interface définissant un moteur de rendu de templates pour les blocs.
* <p>
* Un {@code BlockEngine} est responsable de transformer un contenu template
* en chaîne de caractères finale, en utilisant un contexte de données fourni.
* Le rendu est effectué de manière asynchrone via {@link Uni}.
* <p>
* Les implémentations peuvent utiliser différents moteurs de templates
* (Pebble, Freemarker, Thymeleaf, etc.) selon les besoins.
*
* <h2>Exemple d'implémentation</h2>
* <pre>{@code
* @ApplicationScoped
* public class MyBlockEngine implements BlockEngine {
*
* @Override
* public Uni<String> render(String content, Map<String, Object> context) {
* return Uni.createFrom().item(() -> {
* // Logique de rendu du template
* return renderedContent;
* });
* }
* }
* }</pre>
*
* @see PebbleBlockEngine
*/
public interface BlockEngine {
/**
* Effectue le rendu d'un contenu template avec le contexte fourni.
* <p>
* Cette méthode prend un template sous forme de chaîne de caractères
* et le combine avec les données du contexte pour produire le résultat final.
*
* @param content le contenu du template à rendre, utilisant la syntaxe
* spécifique au moteur de template implémenté
* @param context une Map contenant les variables disponibles pour le template,
* où les clés sont les noms des variables et les valeurs leurs données
* @return un {@link Uni} émettant la chaîne de caractères résultant du rendu,
* ou une erreur si le rendu échoue (template invalide, erreur d'évaluation, etc.)
*/
Uni<String> render(String content, Map<String, Object> context);
}

View File

@@ -0,0 +1,8 @@
package fr.cnd.compositor.blocks.specs;
import fr.cnd.compositor.models.BlockRenderDefinition;
import io.smallrye.mutiny.Uni;
public interface BlockTemplate {
Uni<String> render(BlockRenderDefinition definition);
}

View File

@@ -0,0 +1,8 @@
package fr.cnd.compositor.blocks.specs;
import io.smallrye.mutiny.Uni;
public interface BlockTemplateResolver {
Uni<BlockTemplate> resolveTemplate(String blockIdentifier);
}

View File

@@ -1,24 +0,0 @@
package fr.cnd.compositor.blocks.templates;
import fr.cnd.compositor.blocks.pebble.AbstractPebbleBlockTemplate;
import io.smallrye.common.annotation.Identifier;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
@Identifier("html")
@ApplicationScoped
public class HtmlBlockTemplate extends AbstractPebbleBlockTemplate {
@Override
public Uni<String> getTemplate() {
return Uni.createFrom().item("""
<html>
<head>
<title>{{title}}</title>
</head>
<body>
{{content}}
</body>
</html>
""");
}
}

View File

@@ -1,35 +0,0 @@
package fr.cnd.compositor.blocks.templates;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.cnd.compositor.blocks.specs.BlockConfiguration;
import io.smallrye.common.annotation.Identifier;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.HashMap;
import java.util.Map;
@Identifier("product")
@ApplicationScoped
public class HtmlProductConfiguration implements BlockConfiguration {
@Inject
ObjectMapper objectMapper;
@Override
public Uni<String> getBlockTemplateName() {
return Uni.createFrom().item("html");
}
@Override
public Uni<Map<String, Object>> mapInputs(Map<String, Object> inputs) {
Map<String, String> product = objectMapper.convertValue(inputs.getOrDefault("product", new HashMap<>()), new TypeReference<>() {});
return Uni.createFrom().item(Map.of(
"title", product.getOrDefault("title", ""),
"content", product.getOrDefault("name", "")
));
}
}

View File

@@ -0,0 +1,9 @@
name: Core
#description: Do something useful.
metadata:
# keywords:
# - core
# guide: ... # To create and publish this guide, see https://github.com/quarkiverse/quarkiverse/wiki#documenting-your-extension
# categories:
# - "miscellaneous"
# status: "preview"