From b205c0d18a769b7f2e9603e5862ffcab574e18b2 Mon Sep 17 00:00:00 2001 From: Guillaume Dugas Date: Thu, 19 Feb 2026 09:58:35 +0100 Subject: [PATCH] Init (1) --- .dockerignore | 5 + .github/workflows/build.yml | 5 + .gitignore | 45 ++ .mvn/settings.xml | 19 + .mvn/wrapper/maven-wrapper.properties | 3 + Blocks.http | 47 ++ README.md | 67 +++ bom/pom.xml | 24 +- domain/pom.xml | 29 + .../models/BlockRenderDefinition.java | 2 +- .../models/BlockRenderRequest.java | 2 +- .../compositor}/models/BlockRenderResult.java | 2 +- modules/core/deployment/pom.xml | 63 ++ .../core/deployment/CoreProcessor.java | 14 + .../blocks/blocks/TestBlockTemplate.java | 20 + .../AbstractPebbleBlockTemplateTest.java | 63 ++ .../blocks/pebble/PebbleBlockEngineTest.java | 117 ++++ .../CDIBlockTemplateResolverTest.java | 54 ++ .../blocks/services/BlockServiceTest.java | 57 ++ modules/core/integration-tests/pom.xml | 98 +++ .../cnd/compositor/core/it/CoreResource.java | 32 + .../src/main/resources/application.properties | 0 .../compositor/core/it/CoreResourceIT.java | 7 + .../compositor/core/it/CoreResourceTest.java | 21 + modules/core/pom.xml | 21 + modules/core/runtime/pom.xml | 80 +++ .../cnd/compositor/blocks/BlockResource.java | 60 ++ .../pebble/AbstractPebbleBlockTemplate.java | 2 +- .../blocks/pebble/PebbleBlockEngine.java | 33 + .../blocks/pebble/PebbleProvider.java | 19 + .../CDIBlockConfigurationResolver.java | 27 + .../resolvers/CDIBlockTemplateResolver.java | 27 + .../resolvers/CDIContextMappingResolver.java | 27 + .../blocks/services/BlockService.java | 161 +++++ .../specs/BlockConfigurationResolver.java | 7 + .../compositor/blocks/specs/BlockEngine.java | 52 ++ .../blocks/specs/BlockTemplate.java | 8 + .../blocks/specs/BlockTemplateResolver.java | 8 + .../blocks/templates/HtmlBlockTemplate.java | 24 - .../templates/HtmlProductConfiguration.java | 35 -- .../resources/META-INF/quarkus-extension.yaml | 9 + mvnw | 295 +++++++++ mvnw.cmd | 189 ++++++ pom.xml | 19 +- tmp/CDC_Billetterie_Tourisme_Paris.docx | Bin 0 -> 43659 bytes tmp/generate_cdc.py | 564 ++++++++++++++++++ 46 files changed, 2397 insertions(+), 66 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .mvn/settings.xml create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 Blocks.http create mode 100644 README.md create mode 100644 domain/pom.xml rename {modules/core/runtime/src/main/java/fr/cnd/compositor/blocks => domain/src/main/java/fr/cnd/compositor}/models/BlockRenderDefinition.java (86%) rename {modules/core/runtime/src/main/java/fr/cnd/compositor/blocks => domain/src/main/java/fr/cnd/compositor}/models/BlockRenderRequest.java (84%) rename {modules/core/runtime/src/main/java/fr/cnd/compositor/blocks => domain/src/main/java/fr/cnd/compositor}/models/BlockRenderResult.java (82%) create mode 100644 modules/core/deployment/pom.xml create mode 100644 modules/core/deployment/src/main/java/fr/cnd/compositor/core/deployment/CoreProcessor.java create mode 100644 modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/blocks/TestBlockTemplate.java create mode 100644 modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplateTest.java create mode 100644 modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/pebble/PebbleBlockEngineTest.java create mode 100644 modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/resolvers/CDIBlockTemplateResolverTest.java create mode 100644 modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/services/BlockServiceTest.java create mode 100644 modules/core/integration-tests/pom.xml create mode 100644 modules/core/integration-tests/src/main/java/fr/cnd/compositor/core/it/CoreResource.java create mode 100644 modules/core/integration-tests/src/main/resources/application.properties create mode 100644 modules/core/integration-tests/src/test/java/fr/cnd/compositor/core/it/CoreResourceIT.java create mode 100644 modules/core/integration-tests/src/test/java/fr/cnd/compositor/core/it/CoreResourceTest.java create mode 100644 modules/core/pom.xml create mode 100644 modules/core/runtime/pom.xml create mode 100644 modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/BlockResource.java create mode 100644 modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/PebbleBlockEngine.java create mode 100644 modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/pebble/PebbleProvider.java create mode 100644 modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIBlockConfigurationResolver.java create mode 100644 modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIBlockTemplateResolver.java create mode 100644 modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/resolvers/CDIContextMappingResolver.java create mode 100644 modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/services/BlockService.java create mode 100644 modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockConfigurationResolver.java create mode 100644 modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockEngine.java create mode 100644 modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockTemplate.java create mode 100644 modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/specs/BlockTemplateResolver.java delete mode 100644 modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/templates/HtmlBlockTemplate.java delete mode 100644 modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/templates/HtmlProductConfiguration.java create mode 100644 modules/core/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100644 tmp/CDC_Billetterie_Tourisme_Paris.docx create mode 100644 tmp/generate_cdc.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..94810d0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +* +!target/*-runner +!target/*-runner.jar +!target/lib/* +!target/quarkus-app/* \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index abd47ed..1254f40 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,6 +13,11 @@ jobs: - name: Check out latest repository code uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: '8' + distribution: 'temurin' + - uses: actions/setup-java@v4 with: java-version: '21' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91a800a --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env + +# Plugin directory +/.quarkus/cli/plugins/ +# TLS Certificates +.certs/ diff --git a/.mvn/settings.xml b/.mvn/settings.xml new file mode 100644 index 0000000..984d060 --- /dev/null +++ b/.mvn/settings.xml @@ -0,0 +1,19 @@ + + + + gitea + + + + Authorization + token ${env.MAVEN_ACCESS_TOKEN} + + + + + + + diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8dea6c2 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/Blocks.http b/Blocks.http new file mode 100644 index 0000000..2f82224 --- /dev/null +++ b/Blocks.http @@ -0,0 +1,47 @@ +### Render a simple template +POST http://localhost:8080/blocks/template/render +Content-Type: application/json + +{ + "definitions": { + "defsId": [ + { + "name": "html", + "inputs": { + "name": "toto" + }, + "slots": { + "content": [ + { + "name": "html", + "inputs": { + "name": "slot content" + } + } + ] + } + } + ] + } +} + +### Render a block configuration +POST http://localhost:8080/blocks/configuration/render +Content-Type: application/json + +{ + "definitions": { + "defsId": [ + { + "name": "product", + "inputs": { + "product": { + "title": "TEA", + "name": "Visiting Eiffel Tower" + } + }, + "slots": {} + } + ] + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..95b40d0 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# ublock + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: + +```shell script +./mvnw quarkus:dev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at . + +## Packaging and running the application + +The application can be packaged using: + +```shell script +./mvnw package +``` + +It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: + +```shell script +./mvnw package -Dquarkus.package.jar.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar target/*-runner.jar`. + +## Creating a native executable + +You can create a native executable using: + +```shell script +./mvnw package -Dnative +``` + +Or, if you don't have GraalVM installed, you can run the native executable build in a container using: + +```shell script +./mvnw package -Dnative -Dquarkus.native.container-build=true +``` + +You can then execute your native executable with: `./target/ublock-1.0-SNAPSHOT-runner` + +If you want to learn more about building native executables, please consult . + +## Related Guides + +- REST ([guide](https://quarkus.io/guides/rest)): A Jakarta REST implementation utilizing build time processing and Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. +- REST Jackson ([guide](https://quarkus.io/guides/rest#json-serialisation)): Jackson serialization support for Quarkus REST. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it + +## Provided Code + +### REST + +Easily start your REST Web Services + +[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) diff --git a/bom/pom.xml b/bom/pom.xml index 5f9fe2a..1fa4052 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -11,14 +11,16 @@ ../pom.xml - bom - Bom pom + bom + Compositor Bom 3.14.1 ${surefire-plugin.version} 21 + 21 + 21 UTF-8 UTF-8 quarkus-bom @@ -29,12 +31,30 @@ 3.5.4 + ${project.version} 1.18.42 4.1.0 + + + fr.cnd.compositor + bom + ${compositor.version} + + + fr.cnd.compositor + compositor + ${compositor.version} + + + fr.cnd.compositor + core + ${compositor.version} + + io.pebbletemplates pebble diff --git a/domain/pom.xml b/domain/pom.xml new file mode 100644 index 0000000..3dd7026 --- /dev/null +++ b/domain/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + fr.cnd.compositor + bom + 1.0-SNAPSHOT + ../bom/pom.xml + + + + 8 + 8 + 8 + + + compositor + Compositor Domain + + + + org.projectlombok + lombok + + + + diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderDefinition.java b/domain/src/main/java/fr/cnd/compositor/models/BlockRenderDefinition.java similarity index 86% rename from modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderDefinition.java rename to domain/src/main/java/fr/cnd/compositor/models/BlockRenderDefinition.java index 8cd5265..e5324ae 100644 --- a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderDefinition.java +++ b/domain/src/main/java/fr/cnd/compositor/models/BlockRenderDefinition.java @@ -1,4 +1,4 @@ -package fr.cnd.compositor.blocks.models; +package fr.cnd.compositor.models; import lombok.*; diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderRequest.java b/domain/src/main/java/fr/cnd/compositor/models/BlockRenderRequest.java similarity index 84% rename from modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderRequest.java rename to domain/src/main/java/fr/cnd/compositor/models/BlockRenderRequest.java index ed32c14..89e14f9 100644 --- a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderRequest.java +++ b/domain/src/main/java/fr/cnd/compositor/models/BlockRenderRequest.java @@ -1,4 +1,4 @@ -package fr.cnd.compositor.blocks.models; +package fr.cnd.compositor.models; import lombok.*; diff --git a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderResult.java b/domain/src/main/java/fr/cnd/compositor/models/BlockRenderResult.java similarity index 82% rename from modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderResult.java rename to domain/src/main/java/fr/cnd/compositor/models/BlockRenderResult.java index 085a1a7..5bce0da 100644 --- a/modules/core/runtime/src/main/java/fr/cnd/compositor/blocks/models/BlockRenderResult.java +++ b/domain/src/main/java/fr/cnd/compositor/models/BlockRenderResult.java @@ -1,4 +1,4 @@ -package fr.cnd.compositor.blocks.models; +package fr.cnd.compositor.models; import lombok.*; diff --git a/modules/core/deployment/pom.xml b/modules/core/deployment/pom.xml new file mode 100644 index 0000000..5416b62 --- /dev/null +++ b/modules/core/deployment/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + + fr.cnd.compositor + core-parent + 1.0-SNAPSHOT + ../pom.xml + + core-deployment + Core - Deployment + + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-rest-jackson-deployment + + + + fr.cnd.compositor + core + ${project.version} + + + io.quarkus + quarkus-junit5 + test + + + io.quarkus + quarkus-junit-internal + test + + + + + + + maven-compiler-plugin + + + default-compile + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + + + diff --git a/modules/core/deployment/src/main/java/fr/cnd/compositor/core/deployment/CoreProcessor.java b/modules/core/deployment/src/main/java/fr/cnd/compositor/core/deployment/CoreProcessor.java new file mode 100644 index 0000000..f8bc893 --- /dev/null +++ b/modules/core/deployment/src/main/java/fr/cnd/compositor/core/deployment/CoreProcessor.java @@ -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); + } +} diff --git a/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/blocks/TestBlockTemplate.java b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/blocks/TestBlockTemplate.java new file mode 100644 index 0000000..3072065 --- /dev/null +++ b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/blocks/TestBlockTemplate.java @@ -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 render(BlockRenderDefinition definition) { + return Uni.createFrom().item("
Test Block
"); + } +} diff --git a/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplateTest.java b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplateTest.java new file mode 100644 index 0000000..4386549 --- /dev/null +++ b/modules/core/deployment/src/test/java/fr/cnd/compositor/blocks/pebble/AbstractPebbleBlockTemplateTest.java @@ -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("
Hello
"); + + String result = template.render(BlockRenderDefinition.builder().build()).await().indefinitely(); + + assertEquals("
Hello
", result); + } + + @Test + void shouldRenderTemplateWithContext() { + AbstractPebbleBlockTemplate template = createTemplate("
Hello {{ name }}
"); + Map context = Map.of("name", "World"); + + String result = template.render(BlockRenderDefinition.builder() + .inputs(context) + .build()).await().indefinitely(); + + assertEquals("
Hello World
", result); + } + + @Test + void shouldRenderTemplateWithEmptyContext() { + AbstractPebbleBlockTemplate template = createTemplate("Static content"); + final Map context = new HashMap<>(); + + String result = template.render(BlockRenderDefinition.builder() + .inputs(context) + .build()).await().indefinitely(); + + assertEquals("Static content", result); + } + + private AbstractPebbleBlockTemplate createTemplate(String templateContent) { + AbstractPebbleBlockTemplate template = new AbstractPebbleBlockTemplate() { + @Override + public Uni 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> mapInputs(Map inputs) { - Map product = objectMapper.convertValue(inputs.getOrDefault("product", new HashMap<>()), new TypeReference<>() {}); - - return Uni.createFrom().item(Map.of( - "title", product.getOrDefault("title", ""), - "content", product.getOrDefault("name", "") - )); - } -} diff --git a/modules/core/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/modules/core/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000..e01f36a --- /dev/null +++ b/modules/core/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -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" diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index cf23d7e..dce6b49 100644 --- a/pom.xml +++ b/pom.xml @@ -7,14 +7,31 @@ root 1.0-SNAPSHOT - Blocks + Compositor Parent pom bom builder + domain modules/core + + + gitea + https://gitea.tech.codeanddata.fr/api/packages/${env.MAVEN_REPO_OWNER}/maven + + + + + gitea + https://gitea.tech.codeanddata.fr/api/packages/${env.MAVEN_REPO_OWNER}/maven + + + gitea + https://gitea.tech.codeanddata.fr/api/packages/${env.MAVEN_REPO_OWNER}/maven + + diff --git a/tmp/CDC_Billetterie_Tourisme_Paris.docx b/tmp/CDC_Billetterie_Tourisme_Paris.docx new file mode 100644 index 0000000000000000000000000000000000000000..d935939fc7daad44cf32f34d45c214d48b51bbd1 GIT binary patch literal 43659 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 z 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 a*q_#+ssapHgMMli2EY`o9@xb{fBg@^BDs

Table des matières]") +p.italic = True + +doc.add_page_break() + +# ═══════════════════════════════════════ +# 1. CONTEXTE ET OBJECTIFS +# ═══════════════════════════════════════ + +doc.add_heading("1. Contexte et objectifs", level=1) + +doc.add_heading("1.1 Contexte du projet", level=2) +doc.add_paragraph( + "Le présent cahier des charges décrit les spécifications fonctionnelles et techniques " + "d'une plateforme de billetterie en ligne dédiée au tourisme parisien. Cette plateforme " + "s'adresse exclusivement aux professionnels du secteur touristique : agences de voyages, " + "tour-opérateurs, réceptifs, autocaristes, comités d'entreprise et organisateurs d'événements." +) +doc.add_paragraph( + "La plateforme doit permettre à ces professionnels de rechercher, réserver et gérer " + "des billets pour l'ensemble des sites touristiques, musées, monuments, croisières, " + "spectacles et expériences disponibles sur la région parisienne." +) + +doc.add_heading("1.2 Objectifs stratégiques", level=2) +items = [ + "Centraliser l'offre touristique parisienne sur un portail B2B unique", + "Simplifier le processus de réservation pour les professionnels", + "Offrir des tarifs négociés et des conditions commerciales adaptées au B2B", + "Fournir des outils d'aide à la vente (contenus, médias, suggestions IA)", + "Assurer une visibilité optimale via une stratégie SEO avancée", + "Garantir une expérience utilisateur fluide sur tous les supports (responsive design)", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("1.3 Périmètre", level=2) +doc.add_paragraph( + "Le périmètre couvre la conception, le développement, le déploiement et la maintenance " + "corrective de la plateforme (application web responsive, back-office d'administration, " + "APIs d'intégration). Les applications mobiles natives sont exclues de cette première version " + "mais pourront faire l'objet d'une évolution ultérieure." +) + +# ═══════════════════════════════════════ +# 2. PUBLIC CIBLE +# ═══════════════════════════════════════ + +doc.add_heading("2. Public cible et personas", level=1) + +doc.add_heading("2.1 Utilisateurs principaux", level=2) +table = doc.add_table(rows=5, cols=3) +table.style = 'Light Grid Accent 1' +headers = ["Persona", "Profil", "Besoins clés"] +for i, h in enumerate(headers): + table.rows[0].cells[i].text = h +data = [ + ("Agent de voyage", "Employé d'agence, réserve pour le compte de ses clients", "Recherche rapide, devis, réservation groupée"), + ("Tour-opérateur", "Conçoit des packages touristiques incluant Paris", "API d'intégration, tarifs volume, allotements"), + ("Réceptif / DMC", "Gère l'accueil de groupes sur Paris", "Planning, gestion de groupes, billetterie multi-sites"), + ("Administrateur", "Gestionnaire de la plateforme", "Back-office, statistiques, gestion des contenus"), +] +for idx, (persona, profil, besoins) in enumerate(data): + row = table.rows[idx + 1] + row.cells[0].text = persona + row.cells[1].text = profil + row.cells[2].text = besoins + +# ═══════════════════════════════════════ +# 3. ESPACE PUBLIC +# ═══════════════════════════════════════ + +doc.add_heading("3. Espace public", level=1) + +doc.add_heading("3.1 Page d'accueil", level=2) +items = [ + "Présentation de la plateforme et de sa proposition de valeur", + "Mise en avant des offres phares et des nouveautés (carrousel, bannières)", + "Moteur de recherche principal (par type d'activité, date, lieu, thématique)", + "Accès aux catégories principales (musées, monuments, croisières, spectacles, etc.)", + "Témoignages et références clients professionnels", + "Call-to-action vers l'inscription / la demande de compte professionnel", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("3.2 Catalogue des offres", level=2) +items = [ + "Navigation par catégorie, thématique, arrondissement ou popularité", + "Fiches produit détaillées : description, photos, informations pratiques, conditions tarifaires", + "Système de filtres avancés (prix, disponibilité, accessibilité PMR, langue)", + "Affichage des disponibilités en temps réel (sous réserve de connexion fournisseur)", + "Suggestions de produits complémentaires (cross-selling assisté par IA)", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("3.3 Pages institutionnelles", level=2) +items = [ + "À propos / Qui sommes-nous", + "Conditions générales de vente (CGV) et mentions légales", + "Politique de confidentialité (RGPD)", + "FAQ et centre d'aide", + "Page contact avec formulaire", + "Blog / Actualités du tourisme parisien (levier SEO)", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +# ═══════════════════════════════════════ +# 4. ESPACE PRIVÉ +# ═══════════════════════════════════════ + +doc.add_heading("4. Espace privé (utilisateurs authentifiés)", level=1) + +doc.add_heading("4.1 Inscription et authentification", level=2) +items = [ + "Formulaire d'inscription avec validation manuelle ou automatique (numéro SIRET, licence d'agence)", + "Authentification sécurisée (email/mot de passe, SSO, 2FA optionnel)", + "Gestion des rôles : administrateur agence, agent, comptable (lecture seule)", + "Récupération et réinitialisation de mot de passe", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("4.2 Dashboard", level=2) +items = [ + "Vue d'ensemble de l'activité : réservations récentes, chiffre d'affaires, alertes", + "Indicateurs clés de performance (KPI) personnalisables", + "Raccourcis vers les actions fréquentes (nouvelle réservation, devis en cours)", + "Notifications et messages de la plateforme", + "Calendrier des réservations à venir", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("4.3 Gestion des réservations", level=2) +items = [ + "Panier multi-produits avec récapitulatif avant validation", + "Réservation instantanée ou sur demande (selon le fournisseur)", + "Historique complet des réservations avec statuts (confirmée, en attente, annulée)", + "Modification et annulation en ligne selon les conditions tarifaires", + "Téléchargement des billets (PDF, e-tickets, QR codes)", + "Génération de bons de commande et factures", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("4.4 Contenus exclusifs", level=2) +items = [ + "Accès à des tarifs préférentiels et promotions réservées aux professionnels", + "Médiathèque professionnelle (photos HD, vidéos, descriptifs éditoriaux libres de droits)", + "Guides et supports de vente téléchargeables (PDF, présentations)", + "Webinaires et formations en ligne sur les produits touristiques", + "Alertes personnalisées sur les nouvelles offres et les disponibilités", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("4.5 Gestion du compte", level=2) +items = [ + "Modification des informations de la société et des utilisateurs", + "Gestion des moyens de paiement (prélèvement, virement, carte bancaire, crédit)", + "Consultation du relevé de compte et de l'encours", + "Paramétrage des préférences de notification", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +# ═══════════════════════════════════════ +# 5. BACK-OFFICE +# ═══════════════════════════════════════ + +doc.add_heading("5. Back-office d'administration", level=1) + +doc.add_heading("5.1 Gestion des utilisateurs", level=2) +items = [ + "CRUD complet sur les comptes professionnels (création, validation, suspension, suppression)", + "Affectation de rôles et permissions granulaires", + "Historique des connexions et journal d'audit", + "Système de validation des inscriptions (workflow d'approbation)", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("5.2 Gestion du catalogue", level=2) +items = [ + "Création et édition des fiches produit (WYSIWYG, médias, métadonnées SEO)", + "Gestion des catégories, tags et thématiques", + "Paramétrage des tarifs, allotements et règles de disponibilité", + "Import/export en masse (CSV, XML, API fournisseurs)", + "Gestion des fournisseurs et des contrats associés", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("5.3 Gestion des commandes et de la facturation", level=2) +items = [ + "Tableau de bord des commandes avec filtres avancés", + "Validation, modification et annulation de commandes", + "Génération automatique de factures et avoirs", + "Suivi des paiements et relances automatiques", + "Export comptable (formats standards : FEC, CSV)", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("5.4 Gestion des contenus éditoriaux (CMS)", level=2) +items = [ + "Éditeur de pages (WYSIWYG) pour les contenus institutionnels et le blog", + "Gestion des bannières, carrousels et mises en avant", + "Planification de publication (programmation à date)", + "Gestion multilingue (français, anglais a minima ; extensible)", + "Prévisualisation avant publication", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("5.5 Statistiques et reporting", level=2) +items = [ + "Tableaux de bord analytiques (ventes, CA, taux de conversion, panier moyen)", + "Rapports exportables (PDF, Excel)", + "Suivi du trafic et du comportement utilisateur (intégration analytics)", + "Rapports par fournisseur, par produit, par client", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +# ═══════════════════════════════════════ +# 6. DESIGN RESPONSIVE +# ═══════════════════════════════════════ + +doc.add_heading("6. Prérequis de design responsive", level=1) + +doc.add_heading("6.1 Approche Mobile-First", level=2) +doc.add_paragraph( + "La plateforme doit être conçue selon une approche Mobile-First, garantissant une " + "expérience optimale sur tous les terminaux. L'interface doit s'adapter de manière fluide " + "aux résolutions suivantes :" +) +table = doc.add_table(rows=5, cols=3) +table.style = 'Light Grid Accent 1' +headers = ["Breakpoint", "Résolution", "Cible"] +for i, h in enumerate(headers): + table.rows[0].cells[i].text = h +data = [ + ("Mobile", "320px — 767px", "Smartphones"), + ("Tablette", "768px — 1023px", "Tablettes, iPad"), + ("Desktop", "1024px — 1439px", "Ordinateurs portables"), + ("Large Desktop", "≥ 1440px", "Écrans larges, moniteurs"), +] +for idx, (bp, res, cible) in enumerate(data): + row = table.rows[idx + 1] + row.cells[0].text = bp + row.cells[1].text = res + row.cells[2].text = cible + +doc.add_heading("6.2 Exigences d'accessibilité", level=2) +items = [ + "Conformité RGAA (Référentiel Général d'Amélioration de l'Accessibilité) niveau AA", + "Navigation au clavier complète", + "Contrastes de couleur conformes aux normes WCAG 2.1", + "Textes alternatifs sur tous les médias", + "Structure sémantique HTML5 (balises header, nav, main, article, etc.)", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("6.3 Performance d'affichage", level=2) +items = [ + "Temps de chargement initial (LCP) inférieur à 2,5 secondes sur mobile 4G", + "Score Lighthouse performance ≥ 90", + "Optimisation des images (formats WebP/AVIF, lazy loading, srcset responsive)", + "Mise en cache front-end (Service Worker optionnel pour le mode hors-ligne partiel)", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("6.4 Charte graphique et UI/UX", level=2) +items = [ + "Design system documenté (composants, typographie, couleurs, iconographie)", + "Maquettes à fournir pour les 4 breakpoints définis", + "Prototypage interactif (Figma ou équivalent) pour validation avant développement", + "Tests utilisateurs à prévoir sur un panel de professionnels du tourisme", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +# ═══════════════════════════════════════ +# 7. OPTIMISATION IA +# ═══════════════════════════════════════ + +doc.add_heading("7. Outillage pour l'optimisation IA", level=1) + +doc.add_heading("7.1 Recommandations intelligentes", level=2) +items = [ + "Moteur de recommandation basé sur le comportement utilisateur (historique de réservations, recherches)", + "Suggestions de cross-selling et up-selling contextualisées", + "Personnalisation dynamique de la page d'accueil selon le profil du professionnel", + "Scoring de pertinence des offres en fonction de la saisonnalité et des tendances", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("7.2 Assistance à la rédaction et au contenu", level=2) +items = [ + "Génération assistée de descriptions produit (fiches, accroches commerciales)", + "Traduction automatique des contenus avec relecture humaine", + "Suggestion de mots-clés et optimisation sémantique des fiches produit", + "Résumé automatique des avis et retours clients", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("7.3 Chatbot et assistance conversationnelle", level=2) +items = [ + "Chatbot intelligent pour l'aide à la recherche et à la réservation", + "Support multilingue (français, anglais, espagnol, allemand a minima)", + "Escalade vers un agent humain si le chatbot ne peut pas répondre", + "Base de connaissances alimentée par les FAQ et la documentation produit", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("7.4 Analyse prédictive", level=2) +items = [ + "Prévision de la demande par produit et par période", + "Détection d'anomalies (pics ou creux inhabituels)", + "Optimisation tarifaire dynamique (yield management assisté)", + "Segmentation automatique des clients professionnels", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +# ═══════════════════════════════════════ +# 8. OPTIMISATION SEO +# ═══════════════════════════════════════ + +doc.add_heading("8. Outillage pour l'optimisation SEO", level=1) + +doc.add_heading("8.1 SEO technique", level=2) +items = [ + "Rendu côté serveur (SSR) ou pré-rendu statique (SSG) pour l'indexabilité des pages", + "URLs propres, lisibles et canoniques (ex: /musees/louvre-billets-coupe-file)", + "Balisage sémantique HTML5 et données structurées Schema.org (Product, Offer, Event, BreadcrumbList)", + "Sitemap XML dynamique et fichier robots.txt configurables depuis le back-office", + "Gestion automatique des redirections 301 lors de modifications d'URL", + "Temps de réponse serveur (TTFB) inférieur à 200ms", + "Support natif du protocole HTTPS et HTTP/2", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("8.2 SEO on-page", level=2) +items = [ + "Balises title et meta description éditables par page depuis le back-office", + "Gestion des balises Hn (H1 unique par page, hiérarchie respectée)", + "Attributs alt éditables sur toutes les images", + "Fil d'Ariane (breadcrumb) sur toutes les pages avec balisage structuré", + "Pagination SEO-friendly (rel=prev/next, canonical)", + "Gestion du contenu dupliqué (canonical, hreflang pour le multilingue)", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("8.3 SEO éditorial", level=2) +items = [ + "Module de blog intégré avec catégorisation et maillage interne", + "Outils d'analyse sémantique intégrés (densité de mots-clés, lisibilité)", + "Suggestions IA de mots-clés longue traîne liés au tourisme parisien", + "Gestion du maillage interne assistée (suggestions de liens contextuels)", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("8.4 Suivi et monitoring SEO", level=2) +items = [ + "Intégration Google Search Console et Google Analytics 4", + "Tableau de bord SEO dans le back-office (positions, impressions, CTR)", + "Alertes automatiques en cas de dégradation (erreurs 404, chute de trafic, pages non indexées)", + "Audit SEO automatisé périodique avec recommandations", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +# ═══════════════════════════════════════ +# 9. EXIGENCES TECHNIQUES +# ═══════════════════════════════════════ + +doc.add_heading("9. Exigences techniques", level=1) + +doc.add_heading("9.1 Architecture", level=2) +items = [ + "Architecture microservices ou modulaire découplée", + "API REST et/ou GraphQL documentée (OpenAPI / Swagger)", + "Séparation front-end / back-end (headless CMS recommandé)", + "Conteneurisation (Docker) et orchestration (Kubernetes ou équivalent)", + "Infrastructure cloud (AWS, GCP ou Azure) avec auto-scaling", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("9.2 Sécurité", level=2) +items = [ + "Chiffrement des données en transit (TLS 1.3) et au repos", + "Conformité RGPD : consentement, droit à l'oubli, portabilité des données", + "Protection OWASP Top 10 (injection SQL, XSS, CSRF, etc.)", + "Authentification OAuth 2.0 / OpenID Connect", + "Audits de sécurité annuels et tests de pénétration", + "Journalisation des accès et des actions sensibles", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("9.3 Performance et disponibilité", level=2) +items = [ + "Disponibilité cible : 99,9% (hors maintenance planifiée)", + "Capacité de montée en charge : supporter 10 000 utilisateurs simultanés", + "CDN pour les ressources statiques", + "Stratégie de cache multi-niveaux (CDN, reverse proxy, applicatif)", + "Plan de reprise d'activité (PRA) et plan de continuité d'activité (PCA)", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +doc.add_heading("9.4 Intégrations tierces", level=2) +items = [ + "Passerelle de paiement (Stripe, Adyen ou équivalent) — CB, SEPA, virement", + "Connecteurs fournisseurs (APIs billetterie des sites touristiques)", + "Intégration CRM (Salesforce, HubSpot ou équivalent)", + "Outils d'emailing transactionnel et marketing (SendGrid, Brevo, etc.)", + "Intégration comptable (export FEC, connecteur ERP optionnel)", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +# ═══════════════════════════════════════ +# 10. LIVRABLES ET PLANNING +# ═══════════════════════════════════════ + +doc.add_heading("10. Livrables attendus", level=1) + +table = doc.add_table(rows=9, cols=3) +table.style = 'Light Grid Accent 1' +headers = ["Livrable", "Format", "Phase"] +for i, h in enumerate(headers): + table.rows[0].cells[i].text = h +data = [ + ("Spécifications techniques détaillées", "Document (PDF/DOCX)", "Conception"), + ("Maquettes UI/UX", "Figma", "Conception"), + ("Prototype interactif", "Figma / URL de staging", "Conception"), + ("Code source", "Dépôt Git", "Développement"), + ("Documentation technique (API, architecture)", "Markdown / Swagger", "Développement"), + ("Jeux de tests (unitaires, intégration, E2E)", "Code source", "Développement"), + ("Guide d'administration du back-office", "PDF / Wiki", "Livraison"), + ("Plan de mise en production", "Document", "Livraison"), +] +for idx, (livrable, fmt, phase) in enumerate(data): + row = table.rows[idx + 1] + row.cells[0].text = livrable + row.cells[1].text = fmt + row.cells[2].text = phase + +# ═══════════════════════════════════════ +# 11. CRITÈRES D'ACCEPTATION +# ═══════════════════════════════════════ + +doc.add_heading("11. Critères d'acceptation", level=1) +items = [ + "L'ensemble des fonctionnalités décrites dans ce cahier des charges est opérationnel", + "Les tests automatisés couvrent au minimum 80% du code métier", + "Le score Lighthouse est supérieur ou égal à 90 sur les 4 métriques (Performance, Accessibilité, SEO, Best Practices)", + "Les temps de réponse sont conformes aux seuils définis (TTFB < 200ms, LCP < 2,5s)", + "La plateforme est conforme au RGAA niveau AA", + "Un audit de sécurité a été réalisé sans vulnérabilité critique ou haute non corrigée", + "La documentation technique et utilisateur est complète et à jour", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') + +# ═══════════════════════════════════════ +# 12. ANNEXES +# ═══════════════════════════════════════ + +doc.add_heading("12. Annexes", level=1) +items = [ + "Annexe A — Glossaire des termes métier", + "Annexe B — Liste des sites touristiques et fournisseurs cibles", + "Annexe C — Benchmark concurrentiel", + "Annexe D — Wireframes préliminaires", + "Annexe E — Matrice des rôles et permissions", +] +for item in items: + doc.add_paragraph(item, style='List Bullet') +doc.add_paragraph() +p = doc.add_paragraph("[Contenus des annexes à compléter]") +p.italic = True + +# ── Sauvegarde ── + +output_path = "/Users/guillaume/workspace/codeanddata.fr/projects/uBlock/tmp/CDC_Billetterie_Tourisme_Paris.docx" +doc.save(output_path) +print(f"Document généré : {output_path}") \ No newline at end of file