getTemplate() {
+ return Uni.createFrom().item(templateContent);
+ }
+ };
+ template.setEngine(engine);
+ return template;
+ }
+}
diff --git a/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/pebble/PebbleBlockEngineTest.java b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/pebble/PebbleBlockEngineTest.java
new file mode 100644
index 0000000..e4dfa5b
--- /dev/null
+++ b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/pebble/PebbleBlockEngineTest.java
@@ -0,0 +1,117 @@
+package fr.cnd.compositor.blocks.pebble;
+
+import fr.cnd.compositor.blocks.specs.BlockEngine;
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests unitaires pour {@link PebbleBlockEngine}.
+ *
+ * Cette classe vérifie le comportement du moteur de rendu Pebble
+ * qui implémente l'interface {@link BlockEngine}.
+ */
+@QuarkusTest
+class PebbleBlockEngineTest {
+
+ @Inject
+ PebbleBlockEngine pebbleBlockEngine;
+
+ @Test
+ void shouldRenderSimpleTemplate() {
+ String content = "Hello World";
+
+ String result = pebbleBlockEngine.render(content, Collections.emptyMap())
+ .await().indefinitely();
+
+ assertEquals("Hello World", result);
+ }
+
+ @Test
+ void shouldRenderTemplateWithVariable() {
+ String content = "Hello {{ name }}!";
+ Map context = Map.of("name", "Guillaume");
+
+ String result = pebbleBlockEngine.render(content, context)
+ .await().indefinitely();
+
+ assertEquals("Hello Guillaume!", result);
+ }
+
+ @Test
+ void shouldRenderTemplateWithMultipleVariables() {
+ String content = "{{ greeting }} {{ name }}, you have {{ count }} messages.";
+ Map context = Map.of(
+ "greeting", "Bonjour",
+ "name", "Alice",
+ "count", 5
+ );
+
+ String result = pebbleBlockEngine.render(content, context)
+ .await().indefinitely();
+
+ assertEquals("Bonjour Alice, you have 5 messages.", result);
+ }
+
+ @Test
+ void shouldRenderTemplateWithConditional() {
+ String content = "{% if active %}Active{% else %}Inactive{% endif %}";
+
+ String resultActive = pebbleBlockEngine.render(content, Map.of("active", true))
+ .await().indefinitely();
+ String resultInactive = pebbleBlockEngine.render(content, Map.of("active", false))
+ .await().indefinitely();
+
+ assertEquals("Active", resultActive);
+ assertEquals("Inactive", resultInactive);
+ }
+
+ @Test
+ void shouldRenderTemplateWithLoop() {
+ String content = "{% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}";
+ Map context = Map.of("items", java.util.List.of("A", "B", "C"));
+
+ String result = pebbleBlockEngine.render(content, context)
+ .await().indefinitely();
+
+ assertEquals("A, B, C", result);
+ }
+
+ @Test
+ void shouldHandleEmptyContext() {
+ String content = "Static content only";
+
+ String result = pebbleBlockEngine.render(content, Collections.emptyMap())
+ .await().indefinitely();
+
+ assertEquals("Static content only", result);
+ }
+
+ @Test
+ void shouldHandleMissingVariableGracefully() {
+ String content = "Hello {{ name }}!";
+
+ String result = pebbleBlockEngine.render(content, Collections.emptyMap())
+ .await().indefinitely();
+
+ assertEquals("Hello !", result);
+ }
+
+ @Test
+ void shouldThrowExceptionForInvalidTemplate() {
+ String content = "{% invalid syntax %}";
+
+ RuntimeException exception = assertThrows(RuntimeException.class, () ->
+ pebbleBlockEngine.render(content, Collections.emptyMap())
+ .await().indefinitely()
+ );
+
+ assertTrue(exception.getMessage().contains("Failed to render template content"));
+ }
+
+}
diff --git a/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/resolvers/CDIBlockTemplateResolverTest.java b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/resolvers/CDIBlockTemplateResolverTest.java
new file mode 100644
index 0000000..b1b4799
--- /dev/null
+++ b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/resolvers/CDIBlockTemplateResolverTest.java
@@ -0,0 +1,54 @@
+package fr.cnd.compositor.blocks.resolvers;
+
+import fr.cnd.compositor.models.BlockRenderDefinition;
+import fr.cnd.compositor.blocks.specs.BlockTemplate;
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests unitaires pour {@link CDIBlockTemplateResolver}.
+ *
+ * Cette classe vérifie le comportement du resolver qui recherche
+ * les instances de {@link BlockTemplate} par leur annotation {@code @Identifier}.
+ */
+@QuarkusTest
+class CDIBlockTemplateResolverTest {
+
+ @Inject
+ CDIBlockTemplateResolver resolver;
+
+ /**
+ * Vérifie que le resolver retourne null lorsque le template n'existe pas.
+ */
+ @Test
+ void shouldReturnNullWhenTemplateNotFound() {
+ BlockTemplate result = resolver.resolveTemplate("unknown-template").await().indefinitely();
+
+ assertNull(result);
+ }
+
+ /**
+ * Vérifie que le resolver trouve et retourne le template correspondant à l'identifiant.
+ */
+ @Test
+ void shouldResolveTemplateTemplateByIdentifier() {
+ BlockTemplate result = resolver.resolveTemplate("test-block").await().indefinitely();
+
+ assertNotNull(result);
+ }
+
+ /**
+ * Vérifie que le template résolu peut effectuer un rendu.
+ */
+ @Test
+ void shouldResolveTemplateWorkingTemplate() {
+ BlockTemplate result = resolver.resolveTemplate("test-block").await().indefinitely();
+
+ assertNotNull(result);
+ String rendered = result.render(BlockRenderDefinition.builder().build()).await().indefinitely();
+ assertEquals("
Test Block
", rendered);
+ }
+}
diff --git a/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/services/BlockServiceTest.java b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/services/BlockServiceTest.java
new file mode 100644
index 0000000..5e8d18c
--- /dev/null
+++ b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/services/BlockServiceTest.java
@@ -0,0 +1,57 @@
+package fr.cnd.compositor.blocks.services;
+
+import fr.cnd.compositor.models.BlockRenderDefinition;
+import fr.cnd.compositor.blocks.specs.BlockTemplate;
+import fr.cnd.compositor.blocks.specs.BlockTemplateResolver;
+import io.quarkus.test.junit.QuarkusTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests unitaires pour {@link BlockService}.
+ *
+ * Cette classe vérifie le comportement du service de résolution des templates
+ * qui parcourt les différents {@link BlockTemplateResolver}
+ * pour trouver le premier template correspondant.
+ */
+@QuarkusTest
+class BlockServiceTest {
+
+ @Inject
+ BlockService blockService;
+
+ /**
+ * Vérifie que le service retourne null lorsque aucun resolver ne trouve le template.
+ */
+ @Test
+ void shouldReturnNullWhenNoResolverFindsTemplate() {
+ BlockTemplate result = blockService.resolveTemplate("unknown-template").await().indefinitely();
+
+ assertNull(result);
+ }
+
+ /**
+ * Vérifie que le service trouve un template existant via les resolvers.
+ */
+ @Test
+ void shouldResolveTemplateExistingTemplate() {
+ BlockTemplate result = blockService.resolveTemplate("test-block").await().indefinitely();
+
+ assertNotNull(result);
+ }
+
+ /**
+ * Vérifie que le template résolu peut effectuer un rendu correct.
+ */
+ @Test
+ void shouldResolveTemplateWorkingTemplate() {
+ BlockTemplate result = blockService.resolveTemplate("test-block").await().indefinitely();
+
+ assertNotNull(result);
+ String rendered = result.render(BlockRenderDefinition.builder().build()).await().indefinitely();
+ assertEquals("
Test Block
", rendered);
+ }
+
+}
diff --git a/modules/core/integration-tests/pom.xml b/modules/core/integration-tests/pom.xml
new file mode 100644
index 0000000..885bcdd
--- /dev/null
+++ b/modules/core/integration-tests/pom.xml
@@ -0,0 +1,98 @@
+
+
+ 4.0.0
+
+
+ fr.cnd.compositor
+ core-parent
+ 1.0.0-SNAPSHOT
+
+ core-integration-tests
+ Core - Integration Tests
+
+
+ true
+
+
+
+
+ io.quarkus
+ quarkus-rest
+
+
+ fr.cnd.compositor
+ core
+ ${project.version}
+
+
+ io.quarkus
+ quarkus-junit
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+
+
+
+ build
+
+
+
+
+
+ maven-failsafe-plugin
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}-runner
+ org.jboss.logmanager.LogManager
+ ${maven.home}
+
+
+
+
+
+
+
+
+ native-image
+
+
+ native
+
+
+
+
+
+ maven-surefire-plugin
+
+ ${native.surefire.skip}
+
+
+
+
+
+ false
+ false
+ true
+
+
+
+
diff --git a/modules/core/integration-tests/src/main/java/fr/cnd/compositor/core/it/CoreResource.java b/modules/core/integration-tests/src/main/java/fr/cnd/compositor/core/it/CoreResource.java
new file mode 100644
index 0000000..3352c66
--- /dev/null
+++ b/modules/core/integration-tests/src/main/java/fr/cnd/compositor/core/it/CoreResource.java
@@ -0,0 +1,32 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You under the Apache License, Version 2.0
+* (the "License"); you may not use this file except in compliance with
+* the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+package fr.cnd.compositor.core.it;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+@Path("/core")
+@ApplicationScoped
+public class CoreResource {
+ // add some rest methods here
+
+ @GET
+ public String hello() {
+ return "Hello core";
+ }
+}
diff --git a/modules/core/integration-tests/src/main/resources/application.properties b/modules/core/integration-tests/src/main/resources/application.properties
new file mode 100644
index 0000000..e69de29
diff --git a/modules/core/integration-tests/src/test/java/fr/cnd/compositor/core/it/CoreResourceIT.java b/modules/core/integration-tests/src/test/java/fr/cnd/compositor/core/it/CoreResourceIT.java
new file mode 100644
index 0000000..13bbe66
--- /dev/null
+++ b/modules/core/integration-tests/src/test/java/fr/cnd/compositor/core/it/CoreResourceIT.java
@@ -0,0 +1,7 @@
+package fr.cnd.compositor.core.it;
+
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+
+@QuarkusIntegrationTest
+public class CoreResourceIT extends CoreResourceTest {
+}
diff --git a/modules/core/integration-tests/src/test/java/fr/cnd/compositor/core/it/CoreResourceTest.java b/modules/core/integration-tests/src/test/java/fr/cnd/compositor/core/it/CoreResourceTest.java
new file mode 100644
index 0000000..bfa3f75
--- /dev/null
+++ b/modules/core/integration-tests/src/test/java/fr/cnd/compositor/core/it/CoreResourceTest.java
@@ -0,0 +1,21 @@
+package fr.cnd.compositor.core.it;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.is;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+@QuarkusTest
+public class CoreResourceTest {
+
+ @Test
+ public void testHelloEndpoint() {
+ given()
+ .when().get("/core")
+ .then()
+ .statusCode(200)
+ .body(is("Hello core"));
+ }
+}
diff --git a/modules/core/pom.xml b/modules/core/pom.xml
new file mode 100644
index 0000000..5804cca
--- /dev/null
+++ b/modules/core/pom.xml
@@ -0,0 +1,21 @@
+
+
+ 4.0.0
+
+ fr.cnd.compositor
+ builder
+ 1.0-SNAPSHOT
+ ../../builder/pom.xml
+
+
+ pom
+ core-parent
+ Core - Parent
+
+
+ deployment
+ runtime
+
+
+
diff --git a/modules/core/runtime/pom.xml b/modules/core/runtime/pom.xml
new file mode 100644
index 0000000..a8b09ae
--- /dev/null
+++ b/modules/core/runtime/pom.xml
@@ -0,0 +1,80 @@
+
+
+ 4.0.0
+
+
+ fr.cnd.compositor
+ core-parent
+ 1.0-SNAPSHOT
+ ../pom.xml
+
+ core
+ Core - Runtime
+
+
+
+ fr.cnd.compositor
+ compositor
+
+
+ io.quarkus
+ quarkus-arc
+
+
+ io.quarkus
+ quarkus-rest-jackson
+
+
+ io.pebbletemplates
+ pebble
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+ io.quarkus
+ quarkus-extension-maven-plugin
+ ${quarkus.version}
+
+
+ compile
+
+ extension-descriptor
+
+
+ ${project.groupId}:${project.artifactId}-deployment:${project.version}
+
+
+
+
+
+ maven-compiler-plugin
+
+
+ default-compile
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ io.quarkus
+ quarkus-extension-processor
+
+
+ true
+
+
+
+
+
+
+
diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/BlockResource.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/BlockResource.java
new file mode 100644
index 0000000..62f82b5
--- /dev/null
+++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/BlockResource.java
@@ -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 render(BlockRenderRequest request) {
+ final Map> result = new HashMap<>();
+ Uni chain = Uni.createFrom().voidItem();
+ for (final String definitionId : request.getDefinitions().keySet()) {
+ final List 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 renderConfiguration(BlockRenderRequest request) {
+ final Map> result = new HashMap<>();
+ Uni chain = Uni.createFrom().voidItem();
+ for (final String definitionId : request.getDefinitions().keySet()) {
+ final List 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());
+ }
+}
diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplate.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplate.java
index ea394b4..0e5bb2c 100644
--- a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplate.java
+++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplate.java
@@ -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;
diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/PebbleBlockEngine.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/PebbleBlockEngine.java
new file mode 100644
index 0000000..aab04b4
--- /dev/null
+++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/PebbleBlockEngine.java
@@ -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 render(String content, Map 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);
+ }
+ }));
+ }
+}
diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/PebbleProvider.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/PebbleProvider.java
new file mode 100644
index 0000000..558fb1c
--- /dev/null
+++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/PebbleProvider.java
@@ -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();
+ }
+}
diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIBlockConfigurationResolver.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIBlockConfigurationResolver.java
new file mode 100644
index 0000000..2769b93
--- /dev/null
+++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIBlockConfigurationResolver.java
@@ -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 blockConfigurations;
+
+ @Override
+ public Uni resolveConfiguration(String blockIdentifier) {
+ Instance selected = blockConfigurations.select(Identifier.Literal.of(blockIdentifier));
+ if (selected.isResolvable()) {
+ return Uni.createFrom().item(selected.get());
+ }
+ return Uni.createFrom().nullItem();
+ }
+}
diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIBlockTemplateResolver.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIBlockTemplateResolver.java
new file mode 100644
index 0000000..0a34cbf
--- /dev/null
+++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIBlockTemplateResolver.java
@@ -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 blockTemplates;
+
+ @Override
+ public Uni resolveTemplate(String blockIdentifier) {
+ Instance selected = blockTemplates.select(Identifier.Literal.of(blockIdentifier));
+ if (selected.isResolvable()) {
+ return Uni.createFrom().item(selected.get());
+ }
+ return Uni.createFrom().nullItem();
+ }
+}
diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIContextMappingResolver.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIContextMappingResolver.java
new file mode 100644
index 0000000..e88a1d8
--- /dev/null
+++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIContextMappingResolver.java
@@ -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 contextMappings;
+
+ @Override
+ public Uni resolveContextMapping(String contextMappingIdentifier) {
+ Instance selected = contextMappings.select(Identifier.Literal.of(contextMappingIdentifier));
+ if (selected.isResolvable()) {
+ return Uni.createFrom().item(selected.get());
+ }
+ return Uni.createFrom().nullItem();
+ }
+}
diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/services/BlockService.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/services/BlockService.java
new file mode 100644
index 0000000..f4fa447
--- /dev/null
+++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/services/BlockService.java
@@ -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.
+ *
+ * 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 resolvers;
+
+ @Inject
+ @All
+ List blockConfigurationResolvers;
+
+ /**
+ * Resolves a block template by its name.
+ *
+ * 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 resolveTemplate(String templateName) {
+ Uni 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.
+ *
+ * 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 resolveBlockConfiguration(String blockIdentifier) {
+ Uni 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 renderTemplate(List definitions) {
+ final List results = new ArrayList<>();
+ Uni 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 renderTemplate(BlockRenderDefinition definition) {
+ final Map slotsContent = new HashMap<>();
+ Uni 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 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 renderBlockConfiguration(List definitions) {
+ return mapDefinition(definitions)
+ .chain(this::renderTemplate);
+ }
+
+ private Uni> mapDefinition(List definitions) {
+ final List results = new ArrayList<>();
+ Uni chain = Uni.createFrom().voidItem();
+ for (BlockRenderDefinition definition : definitions) {
+ chain = chain.chain(() -> mapDefinition(definition))
+ .invoke(results::add)
+ .replaceWithVoid();
+ }
+
+ return chain.map(x -> results);
+ }
+
+ private Uni mapDefinition(BlockRenderDefinition definition) {
+ final Map> slotMapping = new HashMap<>();
+ Uni slotChain = Uni.createFrom().voidItem();
+ for (String slotName : Optional.ofNullable(definition.getSlots()).map(Map::keySet).orElse(new HashSet<>())) {
+ final List 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));
+ }
+
+}
diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockConfigurationResolver.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockConfigurationResolver.java
new file mode 100644
index 0000000..138011d
--- /dev/null
+++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockConfigurationResolver.java
@@ -0,0 +1,7 @@
+package fr.cnd.compositor.blocks.specs;
+
+import io.smallrye.mutiny.Uni;
+
+public interface BlockConfigurationResolver {
+ Uni resolveConfiguration(String blockIdentifier);
+}
diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockEngine.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockEngine.java
new file mode 100644
index 0000000..6a3c59e
--- /dev/null
+++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockEngine.java
@@ -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.
+ *
+ * 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}.
+ *
+ * Les implémentations peuvent utiliser différents moteurs de templates
+ * (Pebble, Freemarker, Thymeleaf, etc.) selon les besoins.
+ *
+ *
Exemple d'implémentation
+ * {@code
+ * @ApplicationScoped
+ * public class MyBlockEngine implements BlockEngine {
+ *
+ * @Override
+ * public Uni render(String content, Map context) {
+ * return Uni.createFrom().item(() -> {
+ * // Logique de rendu du template
+ * return renderedContent;
+ * });
+ * }
+ * }
+ * }
+ *
+ * @see PebbleBlockEngine
+ */
+public interface BlockEngine {
+
+ /**
+ * Effectue le rendu d'un contenu template avec le contexte fourni.
+ *
+ * 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 render(String content, Map context);
+}
diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockTemplate.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockTemplate.java
new file mode 100644
index 0000000..5cb1227
--- /dev/null
+++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockTemplate.java
@@ -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 render(BlockRenderDefinition definition);
+}
diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockTemplateResolver.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockTemplateResolver.java
new file mode 100644
index 0000000..99a3d84
--- /dev/null
+++ b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockTemplateResolver.java
@@ -0,0 +1,8 @@
+package fr.cnd.compositor.blocks.specs;
+
+import io.smallrye.mutiny.Uni;
+
+public interface BlockTemplateResolver {
+ Uni resolveTemplate(String blockIdentifier);
+}
+
diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/templates/HtmlBlockTemplate.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/templates/HtmlBlockTemplate.java
deleted file mode 100644
index f499321..0000000
--- a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/templates/HtmlBlockTemplate.java
+++ /dev/null
@@ -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 getTemplate() {
- return Uni.createFrom().item("""
-
-
- {{title}}
-
-
- {{content}}
-
-
- """);
- }
-}
diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/templates/HtmlProductConfiguration.java b/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/templates/HtmlProductConfiguration.java
deleted file mode 100644
index ddd43b7..0000000
--- a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/templates/HtmlProductConfiguration.java
+++ /dev/null
@@ -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 getBlockTemplateName() {
- return Uni.createFrom().item("html");
- }
-
- @Override
- public Uni