Init (1)
Some checks failed
Maven build / build (push) Failing after 22s

This commit is contained in:
Guillaume Dugas
2026-02-19 09:58:35 +01:00
parent 7ad05e5b14
commit d389cc2019
43 changed files with 2376 additions and 7 deletions

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.cnd.compositor</groupId>
<artifactId>core-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>core-deployment</artifactId>
<name>Core - Deployment</name>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson-deployment</artifactId>
</dependency>
<dependency>
<groupId>fr.cnd.compositor</groupId>
<artifactId>core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit-internal</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${quarkus.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,14 @@
package fr.cnd.compositor.core.deployment;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.FeatureBuildItem;
class CoreProcessor {
private static final String FEATURE = "core";
@BuildStep
FeatureBuildItem feature() {
return new FeatureBuildItem(FEATURE);
}
}

View File

@@ -0,0 +1,20 @@
package fr.cnd.compositor.blocks.blocks;
import fr.cnd.compositor.models.BlockRenderDefinition;
import fr.cnd.compositor.blocks.specs.BlockTemplate;
import io.smallrye.common.annotation.Identifier;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
/**
* Implémentation de test pour {@link BlockTemplate}.
*/
@Identifier("test-block")
@ApplicationScoped
public class TestBlockTemplate implements BlockTemplate {
@Override
public Uni<String> render(BlockRenderDefinition definition) {
return Uni.createFrom().item("<div>Test Block</div>");
}
}

View File

@@ -0,0 +1,63 @@
package fr.cnd.compositor.blocks.pebble;
import fr.cnd.compositor.models.BlockRenderDefinition;
import io.quarkus.test.junit.QuarkusTest;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
@QuarkusTest
class AbstractPebbleBlockTemplateTest {
@Inject
PebbleBlockEngine engine;
@Test
void shouldRenderStaticTemplate() {
AbstractPebbleBlockTemplate template = createTemplate("<div>Hello</div>");
String result = template.render(BlockRenderDefinition.builder().build()).await().indefinitely();
assertEquals("<div>Hello</div>", result);
}
@Test
void shouldRenderTemplateWithContext() {
AbstractPebbleBlockTemplate template = createTemplate("<div>Hello {{ name }}</div>");
Map<String, Object> context = Map.of("name", "World");
String result = template.render(BlockRenderDefinition.builder()
.inputs(context)
.build()).await().indefinitely();
assertEquals("<div>Hello World</div>", result);
}
@Test
void shouldRenderTemplateWithEmptyContext() {
AbstractPebbleBlockTemplate template = createTemplate("<span>Static content</span>");
final Map<String, Object> context = new HashMap<>();
String result = template.render(BlockRenderDefinition.builder()
.inputs(context)
.build()).await().indefinitely();
assertEquals("<span>Static content</span>", result);
}
private AbstractPebbleBlockTemplate createTemplate(String templateContent) {
AbstractPebbleBlockTemplate template = new AbstractPebbleBlockTemplate() {
@Override
public Uni<String> getTemplate() {
return Uni.createFrom().item(templateContent);
}
};
template.setEngine(engine);
return template;
}
}

View File

@@ -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}.
* <p>
* 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<String, Object> 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<String, Object> 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<String, Object> 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"));
}
}

View File

@@ -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}.
* <p>
* 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("<div>Test Block</div>", rendered);
}
}

View File

@@ -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}.
* <p>
* 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("<div>Test Block</div>", rendered);
}
}

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.cnd.compositor</groupId>
<artifactId>core-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>core-integration-tests</artifactId>
<name>Core - Integration Tests</name>
<properties>
<skipITs>true</skipITs>
</properties>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>fr.cnd.compositor</groupId>
<artifactId>core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native-image</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>${native.surefire.skip}</skipTests>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<quarkus.package.jar.enabled>false</quarkus.package.jar.enabled>
<skipITs>false</skipITs>
<quarkus.native.enabled>true</quarkus.native.enabled>
</properties>
</profile>
</profiles>
</project>

View File

@@ -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";
}
}

View File

@@ -0,0 +1,7 @@
package fr.cnd.compositor.core.it;
import io.quarkus.test.junit.QuarkusIntegrationTest;
@QuarkusIntegrationTest
public class CoreResourceIT extends CoreResourceTest {
}

View File

@@ -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"));
}
}

21
modules/core/pom.xml Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.cnd.compositor</groupId>
<artifactId>builder</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../../builder/pom.xml</relativePath>
</parent>
<packaging>pom</packaging>
<artifactId>core-parent</artifactId>
<name>Core - Parent</name>
<modules>
<module>deployment</module>
<module>runtime</module>
</modules>
</project>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.cnd.compositor</groupId>
<artifactId>core-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>core</artifactId>
<name>Core - Runtime</name>
<dependencies>
<dependency>
<groupId>fr.cnd.compositor</groupId>
<artifactId>compositor</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.pebbletemplates</groupId>
<artifactId>pebble</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-maven-plugin</artifactId>
<version>${quarkus.version}</version>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>extension-descriptor</goal>
</goals>
<configuration>
<deployment>${project.groupId}:${project.artifactId}-deployment:${project.version}</deployment>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
</path>
</annotationProcessorPaths>
<annotationProcessorPathsUseDepMgmt>true</annotationProcessorPathsUseDepMgmt>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

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

@@ -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"